| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/bindings/core/v8/v8_code_cache.h" |
| |
| #include "base/optional.h" |
| #include "build/build_config.h" |
| #include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h" |
| #include "third_party/blink/public/web/web_settings.h" |
| #include "third_party/blink/renderer/bindings/core/v8/module_record.h" |
| #include "third_party/blink/renderer/bindings/core/v8/referrer_script_info.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_source_code.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_initializer.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_trace_events.h" |
| #include "third_party/blink/renderer/core/probe/core_probes.h" |
| #include "third_party/blink/renderer/platform/instrumentation/histogram.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.h" |
| #include "third_party/blink/renderer/platform/wtf/assertions.h" |
| #include "third_party/blink/renderer/platform/wtf/text/text_encoding.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| enum CacheTagKind { kCacheTagCode = 0, kCacheTagTimeStamp = 1, kCacheTagLast }; |
| |
| static const int kCacheTagKindSize = 1; |
| |
| static_assert((1 << kCacheTagKindSize) >= kCacheTagLast, |
| "CacheTagLast must be large enough"); |
| |
| uint32_t CacheTag(CacheTagKind kind, const String& encoding) { |
| static uint32_t v8_cache_data_version = |
| v8::ScriptCompiler::CachedDataVersionTag() << kCacheTagKindSize; |
| |
| // A script can be (successfully) interpreted with different encodings, |
| // depending on the page it appears in. The cache doesn't know anything |
| // about encodings, but the cached data is specific to one encoding. If we |
| // later load the script from the cache and interpret it with a different |
| // encoding, the cached data is not valid for that encoding. |
| return (v8_cache_data_version | kind) + |
| (encoding.IsNull() ? 0 : StringHash::GetHash(encoding)); |
| } |
| |
| // Check previously stored timestamp. |
| bool IsResourceHotForCaching(const SingleCachedMetadataHandler* cache_handler) { |
| static constexpr base::TimeDelta kHotHours = base::TimeDelta::FromHours(72); |
| scoped_refptr<CachedMetadata> cached_metadata = |
| cache_handler->GetCachedMetadata( |
| V8CodeCache::TagForTimeStamp(cache_handler)); |
| if (!cached_metadata) |
| return false; |
| uint64_t time_stamp_ms; |
| const uint32_t size = sizeof(time_stamp_ms); |
| DCHECK_EQ(cached_metadata->size(), size); |
| memcpy(&time_stamp_ms, cached_metadata->Data(), size); |
| base::TimeTicks time_stamp = |
| base::TimeTicks() + base::TimeDelta::FromMilliseconds(time_stamp_ms); |
| return (base::TimeTicks::Now() - time_stamp) < kHotHours; |
| } |
| |
| } // namespace |
| |
| bool V8CodeCache::HasCodeCache( |
| const SingleCachedMetadataHandler* cache_handler) { |
| if (!cache_handler) |
| return false; |
| |
| uint32_t code_cache_tag = V8CodeCache::TagForCodeCache(cache_handler); |
| return cache_handler->GetCachedMetadata(code_cache_tag).get(); |
| } |
| |
| v8::ScriptCompiler::CachedData* V8CodeCache::CreateCachedData( |
| const SingleCachedMetadataHandler* cache_handler) { |
| DCHECK(cache_handler); |
| uint32_t code_cache_tag = V8CodeCache::TagForCodeCache(cache_handler); |
| scoped_refptr<CachedMetadata> cached_metadata = |
| cache_handler->GetCachedMetadata(code_cache_tag); |
| DCHECK(cached_metadata); |
| const uint8_t* data = cached_metadata->Data(); |
| int length = cached_metadata->size(); |
| return new v8::ScriptCompiler::CachedData( |
| data, length, v8::ScriptCompiler::CachedData::BufferNotOwned); |
| } |
| |
| std::tuple<v8::ScriptCompiler::CompileOptions, |
| V8CodeCache::ProduceCacheOptions, |
| v8::ScriptCompiler::NoCacheReason> |
| V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions cache_options, |
| const ScriptSourceCode& source) { |
| return GetCompileOptions(cache_options, source.CacheHandler(), |
| source.Source().length(), |
| source.SourceLocationType()); |
| } |
| |
| std::tuple<v8::ScriptCompiler::CompileOptions, |
| V8CodeCache::ProduceCacheOptions, |
| v8::ScriptCompiler::NoCacheReason> |
| V8CodeCache::GetCompileOptions(mojom::blink::V8CacheOptions cache_options, |
| const SingleCachedMetadataHandler* cache_handler, |
| size_t source_text_length, |
| ScriptSourceLocationType source_location_type) { |
| static const int kMinimalCodeLength = 1024; |
| v8::ScriptCompiler::NoCacheReason no_cache_reason; |
| |
| switch (source_location_type) { |
| case ScriptSourceLocationType::kInline: |
| no_cache_reason = v8::ScriptCompiler::kNoCacheBecauseInlineScript; |
| break; |
| case ScriptSourceLocationType::kInlineInsideDocumentWrite: |
| no_cache_reason = v8::ScriptCompiler::kNoCacheBecauseInDocumentWrite; |
| break; |
| case ScriptSourceLocationType::kExternalFile: |
| no_cache_reason = |
| v8::ScriptCompiler::kNoCacheBecauseResourceWithNoCacheHandler; |
| break; |
| // TODO(leszeks): Possibly differentiate between the other kinds of script |
| // origin also. |
| default: |
| no_cache_reason = v8::ScriptCompiler::kNoCacheBecauseNoResource; |
| break; |
| } |
| |
| if (!cache_handler) { |
| return std::make_tuple(v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kNoProduceCache, |
| no_cache_reason); |
| } |
| |
| if (cache_options == mojom::blink::V8CacheOptions::kNone) { |
| no_cache_reason = v8::ScriptCompiler::kNoCacheBecauseCachingDisabled; |
| return std::make_tuple(v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kNoProduceCache, |
| no_cache_reason); |
| } |
| |
| if (source_text_length < kMinimalCodeLength) { |
| no_cache_reason = v8::ScriptCompiler::kNoCacheBecauseScriptTooSmall; |
| return std::make_tuple(v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kNoProduceCache, |
| no_cache_reason); |
| } |
| |
| if (HasCodeCache(cache_handler)) { |
| return std::make_tuple(v8::ScriptCompiler::kConsumeCodeCache, |
| ProduceCacheOptions::kNoProduceCache, |
| no_cache_reason); |
| } |
| |
| // If the resource is served from CacheStorage, generate the V8 code cache in |
| // the first load. |
| if (cache_handler->IsServedFromCacheStorage()) |
| cache_options = mojom::blink::V8CacheOptions::kCodeWithoutHeatCheck; |
| |
| switch (cache_options) { |
| case mojom::blink::V8CacheOptions::kDefault: |
| case mojom::blink::V8CacheOptions::kCode: |
| if (!IsResourceHotForCaching(cache_handler)) { |
| return std::make_tuple(v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kSetTimeStamp, |
| v8::ScriptCompiler::kNoCacheBecauseCacheTooCold); |
| } |
| return std::make_tuple( |
| v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kProduceCodeCache, |
| v8::ScriptCompiler::kNoCacheBecauseDeferredProduceCodeCache); |
| case mojom::blink::V8CacheOptions::kCodeWithoutHeatCheck: |
| return std::make_tuple( |
| v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kProduceCodeCache, |
| v8::ScriptCompiler::kNoCacheBecauseDeferredProduceCodeCache); |
| case mojom::blink::V8CacheOptions::kFullCodeWithoutHeatCheck: |
| return std::make_tuple( |
| v8::ScriptCompiler::kEagerCompile, |
| ProduceCacheOptions::kProduceCodeCache, |
| v8::ScriptCompiler::kNoCacheBecauseDeferredProduceCodeCache); |
| case mojom::blink::V8CacheOptions::kNone: |
| // Shouldn't happen, as this is handled above. |
| // Case is here so that compiler can check all cases are handled. |
| NOTREACHED(); |
| break; |
| } |
| |
| // All switch branches should return and we should never get here. |
| // But some compilers aren't sure, hence this default. |
| NOTREACHED(); |
| return std::make_tuple(v8::ScriptCompiler::kNoCompileOptions, |
| ProduceCacheOptions::kNoProduceCache, |
| v8::ScriptCompiler::kNoCacheNoReason); |
| } |
| |
| template <typename UnboundScript> |
| static void ProduceCacheInternal( |
| v8::Isolate* isolate, |
| UnboundScript unbound_script, |
| SingleCachedMetadataHandler* cache_handler, |
| size_t source_text_length, |
| const KURL& source_url, |
| const TextPosition& source_start_position, |
| bool is_streamed, |
| const char* trace_name, |
| V8CodeCache::ProduceCacheOptions produce_cache_options, |
| ScriptStreamer::NotStreamingReason not_streaming_reason) { |
| TRACE_EVENT0("v8", trace_name); |
| RuntimeCallStatsScopedTracer rcs_scoped_tracer(isolate); |
| RUNTIME_CALL_TIMER_SCOPE(isolate, RuntimeCallStats::CounterId::kV8); |
| |
| switch (produce_cache_options) { |
| case V8CodeCache::ProduceCacheOptions::kSetTimeStamp: |
| V8CodeCache::SetCacheTimeStamp(cache_handler); |
| break; |
| case V8CodeCache::ProduceCacheOptions::kProduceCodeCache: { |
| // TODO(crbug.com/938269): Investigate why this can be empty here. |
| if (unbound_script.IsEmpty()) |
| break; |
| |
| constexpr const char* kTraceEventCategoryGroup = "v8,devtools.timeline"; |
| TRACE_EVENT_BEGIN1(kTraceEventCategoryGroup, trace_name, "fileName", |
| source_url.GetString().Utf8()); |
| |
| std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data( |
| v8::ScriptCompiler::CreateCodeCache(unbound_script)); |
| if (cached_data) { |
| const uint8_t* data = cached_data->data; |
| int length = cached_data->length; |
| if (length > 1024) { |
| // Omit histogram samples for small cache data to avoid outliers. |
| int cache_size_ratio = |
| static_cast<int>(100.0 * length / source_text_length); |
| DEFINE_THREAD_SAFE_STATIC_LOCAL( |
| CustomCountHistogram, code_cache_size_histogram, |
| ("V8.CodeCacheSizeRatio", 0, 10000, 50)); |
| code_cache_size_histogram.Count(cache_size_ratio); |
| } |
| cache_handler->ClearCachedMetadata( |
| CachedMetadataHandler::kClearLocally); |
| cache_handler->SetCachedMetadata( |
| V8CodeCache::TagForCodeCache(cache_handler), data, length); |
| } |
| |
| TRACE_EVENT_END1( |
| kTraceEventCategoryGroup, trace_name, "data", |
| inspector_compile_script_event::Data( |
| source_url.GetString(), source_start_position, |
| inspector_compile_script_event::V8CacheResult( |
| inspector_compile_script_event::V8CacheResult::ProduceResult( |
| cached_data ? cached_data->length : 0), |
| base::Optional<inspector_compile_script_event::V8CacheResult:: |
| ConsumeResult>()), |
| is_streamed, not_streaming_reason)); |
| break; |
| } |
| case V8CodeCache::ProduceCacheOptions::kNoProduceCache: |
| break; |
| } |
| } |
| |
| void V8CodeCache::ProduceCache(v8::Isolate* isolate, |
| v8::Local<v8::Script> script, |
| const ScriptSourceCode& source, |
| ProduceCacheOptions produce_cache_options) { |
| ProduceCacheInternal(isolate, script->GetUnboundScript(), |
| source.CacheHandler(), source.Source().length(), |
| source.Url(), source.StartPosition(), source.Streamer(), |
| "v8.compile", produce_cache_options, |
| source.NotStreamingReason()); |
| } |
| |
| void V8CodeCache::ProduceCache(v8::Isolate* isolate, |
| ModuleRecordProduceCacheData* produce_cache_data, |
| size_t source_text_length, |
| const KURL& source_url, |
| const TextPosition& source_start_position) { |
| ProduceCacheInternal(isolate, produce_cache_data->UnboundScript(isolate), |
| produce_cache_data->CacheHandler(), source_text_length, |
| source_url, source_start_position, false, |
| "v8.compileModule", |
| produce_cache_data->GetProduceCacheOptions(), |
| ScriptStreamer::NotStreamingReason::kModuleScript); |
| } |
| |
| uint32_t V8CodeCache::TagForCodeCache( |
| const SingleCachedMetadataHandler* cache_handler) { |
| return CacheTag(kCacheTagCode, cache_handler->Encoding()); |
| } |
| |
| uint32_t V8CodeCache::TagForTimeStamp( |
| const SingleCachedMetadataHandler* cache_handler) { |
| return CacheTag(kCacheTagTimeStamp, cache_handler->Encoding()); |
| } |
| |
| // Store a timestamp to the cache as hint. |
| void V8CodeCache::SetCacheTimeStamp( |
| SingleCachedMetadataHandler* cache_handler) { |
| uint64_t now_ms = base::TimeTicks::Now().since_origin().InMilliseconds(); |
| cache_handler->ClearCachedMetadata(CachedMetadataHandler::kClearLocally); |
| cache_handler->SetCachedMetadata(TagForTimeStamp(cache_handler), |
| reinterpret_cast<uint8_t*>(&now_ms), |
| sizeof(now_ms)); |
| } |
| |
| // static |
| scoped_refptr<CachedMetadata> V8CodeCache::GenerateFullCodeCache( |
| ScriptState* script_state, |
| const String& script_string, |
| const String& file_name, |
| const WTF::TextEncoding& encoding, |
| OpaqueMode opaque_mode) { |
| constexpr const char* kTraceEventCategoryGroup = "v8,devtools.timeline"; |
| TRACE_EVENT_BEGIN1(kTraceEventCategoryGroup, "v8.compile", "fileName", |
| file_name.Utf8()); |
| |
| ScriptState::Scope scope(script_state); |
| v8::Isolate* isolate = script_state->GetIsolate(); |
| // v8::TryCatch is needed to suppress all exceptions thrown during the code |
| // cache generation. |
| v8::TryCatch block(isolate); |
| ReferrerScriptInfo referrer_info; |
| v8::ScriptOrigin origin( |
| V8String(isolate, file_name), |
| 0, // line_offset |
| 0, // column_offset |
| opaque_mode == OpaqueMode::kNotOpaque, // is_shared_cross_origin |
| -1, // script_id |
| V8String(isolate, String("")), // source_map_url |
| opaque_mode == OpaqueMode::kOpaque, // is_opaque |
| false, // is_wasm |
| false, // is_module |
| referrer_info.ToV8HostDefinedOptions(isolate)); |
| v8::Local<v8::String> code(V8String(isolate, script_string)); |
| v8::ScriptCompiler::Source source(code, origin); |
| scoped_refptr<CachedMetadata> cached_metadata; |
| std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data; |
| |
| v8::Local<v8::UnboundScript> unbound_script; |
| // When failed to compile the script with syntax error, the exceptions is |
| // suppressed by the v8::TryCatch, and returns null. |
| if (v8::ScriptCompiler::CompileUnboundScript( |
| isolate, &source, v8::ScriptCompiler::kEagerCompile) |
| .ToLocal(&unbound_script)) { |
| cached_data.reset(v8::ScriptCompiler::CreateCodeCache(unbound_script)); |
| if (cached_data && cached_data->length) { |
| cached_metadata = |
| CachedMetadata::Create(CacheTag(kCacheTagCode, encoding.GetName()), |
| cached_data->data, cached_data->length); |
| } |
| } |
| |
| TRACE_EVENT_END1( |
| kTraceEventCategoryGroup, "v8.compile", "data", |
| inspector_compile_script_event::Data( |
| file_name, TextPosition(), |
| inspector_compile_script_event::V8CacheResult( |
| inspector_compile_script_event::V8CacheResult::ProduceResult( |
| cached_data ? cached_data->length : 0), |
| base::Optional<inspector_compile_script_event::V8CacheResult:: |
| ConsumeResult>()), |
| false, ScriptStreamer::NotStreamingReason::kHasCodeCache)); |
| |
| return cached_metadata; |
| } |
| |
| } // namespace blink |