Commit 25072178 authored by Seth Brenith's avatar Seth Brenith Committed by V8 LUCI CQ

Improve Script reuse in isolate compilation cache, part 1

Once the root SharedFunctionInfo for any Script gets its bytecode
flushed, the Isolate's compilation cache currently evicts that entry, to
reduce memory usage. However, the associated Script is likely still
alive, since scripts often declare functions which outlive the initial
evaluation of the script. If an identical script is loaded later, a
duplicate Script is created for it, which can waste memory.

In this change, I propose that the compilation cache keys can refer
weakly to the Script. When the root SharedFunctionInfo gets old, instead
of deleting the cache entry entirely, we can just drop the strong
reference to the SharedFunctionInfo. A subsequent lookup in the cache
will retrieve the Script instead of the root SharedFunctionInfo,
indicating an opportunity to save some memory by reusing the existing
Script.

Eventually, all callers to CompilationCache::LookupScript should reuse
the Script if possible. This change implements only the easy case of
reusing the Script for synchronous parsing. Follow-up changes will be
required for the TODO comments left by this change.

Bug: v8:12808
Change-Id: Ia8b0389441a682de9a43e73329049fd2e7835d3d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3597106Reviewed-by: 's avatarToon Verwaest <verwaest@chromium.org>
Commit-Queue: Seth Brenith <seth.brenith@microsoft.com>
Reviewed-by: 's avatarLeszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80472}
parent 8fbefa47
......@@ -65,17 +65,75 @@ void CompilationSubCache::AgeByGeneration(CompilationSubCache* c) {
c->tables_[0] = ReadOnlyRoots(c->isolate()).undefined_value();
}
// static
void CompilationSubCache::AgeCustom(CompilationSubCache* c) {
DCHECK_EQ(c->generations(), 1);
if (c->tables_[0].IsUndefined(c->isolate())) return;
CompilationCacheTable::cast(c->tables_[0]).Age(c->isolate());
CompilationCacheTable CompilationSubCache::MaybeGetFirstTable() {
DCHECK_GE(generations(), 1);
if (tables_[0].IsUndefined(isolate())) return {};
return CompilationCacheTable::cast(tables_[0]);
}
void CompilationCacheScript::Age() {
if (FLAG_isolate_script_cache_ageing) AgeCustom(this);
DisallowGarbageCollection no_gc;
if (!FLAG_isolate_script_cache_ageing) return;
DCHECK_EQ(generations(), 1);
CompilationCacheTable table = MaybeGetFirstTable();
if (table.is_null()) return;
for (InternalIndex entry : table.IterateEntries()) {
Object key;
if (!table.ToKey(isolate(), entry, &key)) continue;
DCHECK(key.IsWeakFixedArray());
Object value = table.PrimaryValueAt(entry);
if (!value.IsUndefined(isolate())) {
SharedFunctionInfo info = SharedFunctionInfo::cast(value);
if (!info.is_compiled() || (info.HasBytecodeArray() &&
info.GetBytecodeArray(isolate()).IsOld())) {
table.SetPrimaryValueAt(entry,
ReadOnlyRoots(isolate()).undefined_value());
}
}
}
}
void CompilationCacheEval::Age() { AgeCustom(this); }
void CompilationCacheEval::Age() {
DisallowGarbageCollection no_gc;
DCHECK_EQ(generations(), 1);
CompilationCacheTable table = MaybeGetFirstTable();
if (table.is_null()) return;
for (InternalIndex entry : table.IterateEntries()) {
Object key;
if (!table.ToKey(isolate(), entry, &key)) continue;
if (key.IsNumber(isolate())) {
// The ageing mechanism for the initial dummy entry in the eval cache.
// The 'key' is the hash represented as a Number. The 'value' is a smi
// counting down from kHashGenerations. On reaching zero, the entry is
// cleared.
// Note: The following static assert only establishes an explicit
// connection between initialization- and use-sites of the smi value
// field.
STATIC_ASSERT(CompilationCacheTable::kHashGenerations);
const int new_count = Smi::ToInt(table.PrimaryValueAt(entry)) - 1;
if (new_count == 0) {
table.RemoveEntry(entry);
} else {
DCHECK_GT(new_count, 0);
table.SetPrimaryValueAt(entry, Smi::FromInt(new_count),
SKIP_WRITE_BARRIER);
}
} else {
DCHECK(key.IsFixedArray());
// The ageing mechanism for eval caches.
SharedFunctionInfo info =
SharedFunctionInfo::cast(table.PrimaryValueAt(entry));
if (info.HasBytecodeArray() && info.GetBytecodeArray(isolate()).IsOld()) {
table.RemoveEntry(entry);
}
}
}
}
void CompilationCacheRegExp::Age() { AgeByGeneration(this); }
void CompilationSubCache::Iterate(RootVisitor* v) {
......@@ -110,10 +168,8 @@ namespace {
// We only re-use a cached function for some script source code if the
// script originates from the same place. This is to avoid issues
// when reporting errors, etc.
bool HasOrigin(Isolate* isolate, Handle<SharedFunctionInfo> function_info,
bool HasOrigin(Isolate* isolate, Handle<Script> script,
const ScriptDetails& script_details) {
Handle<Script> script =
Handle<Script>(Script::cast(function_info->script()), isolate);
// If the script name isn't set, the boilerplate script should have
// an undefined name to have the same origin.
Handle<Object> name;
......@@ -165,10 +221,10 @@ bool HasOrigin(Isolate* isolate, Handle<SharedFunctionInfo> function_info,
// be cached in the same script generation. Currently the first use
// will be cached, but subsequent code from different source / line
// won't.
MaybeHandle<SharedFunctionInfo> CompilationCacheScript::Lookup(
Handle<String> source, const ScriptDetails& script_details,
LanguageMode language_mode) {
MaybeHandle<SharedFunctionInfo> result;
CompilationCacheScript::LookupResult CompilationCacheScript::Lookup(
Handle<String> source, const ScriptDetails& script_details) {
LookupResult result;
LookupResult::RawObjects raw_result;
// Probe the script generation tables. Make sure not to leak handles
// into the caller's handle scope.
......@@ -177,28 +233,33 @@ MaybeHandle<SharedFunctionInfo> CompilationCacheScript::Lookup(
const int generation = 0;
DCHECK_EQ(generations(), 1);
Handle<CompilationCacheTable> table = GetTable(generation);
MaybeHandle<SharedFunctionInfo> probe = CompilationCacheTable::LookupScript(
table, source, language_mode, isolate());
Handle<SharedFunctionInfo> function_info;
if (probe.ToHandle(&function_info)) {
// Break when we've found a suitable shared function info that
// matches the origin.
if (HasOrigin(isolate(), function_info, script_details)) {
result = scope.CloseAndEscape(function_info);
LookupResult probe =
CompilationCacheTable::LookupScript(table, source, isolate());
Handle<Script> script;
if (probe.script().ToHandle(&script)) {
// Break when we've found a suitable script that matches the origin.
if (HasOrigin(isolate(), script, script_details)) {
raw_result = probe.GetRawObjects();
}
}
}
result = LookupResult::FromRawObjects(raw_result, isolate());
// Once outside the manacles of the handle scope, we need to recheck
// to see if we actually found a cached script. If so, we return a
// handle created in the caller's handle scope.
Handle<SharedFunctionInfo> function_info;
if (result.ToHandle(&function_info)) {
// Since HasOrigin can allocate, we need to protect the SharedFunctionInfo
// with handles during the call.
DCHECK(HasOrigin(isolate(), function_info, script_details));
Handle<Script> script;
if (result.script().ToHandle(&script)) {
// Since HasOrigin can allocate, we need to protect the lookup result with
// handles during the call.
DCHECK(HasOrigin(isolate(), script, script_details));
Handle<SharedFunctionInfo> sfi;
if (result.toplevel_sfi().ToHandle(&sfi)) {
isolate()->counters()->compilation_cache_hits()->Increment();
LOG(isolate(), CompilationCacheEvent("hit", "script", *function_info));
LOG(isolate(), CompilationCacheEvent("hit", "script", *sfi));
} else {
isolate()->counters()->compilation_cache_partial_hits()->Increment();
}
} else {
isolate()->counters()->compilation_cache_misses()->Increment();
}
......@@ -206,12 +267,11 @@ MaybeHandle<SharedFunctionInfo> CompilationCacheScript::Lookup(
}
void CompilationCacheScript::Put(Handle<String> source,
LanguageMode language_mode,
Handle<SharedFunctionInfo> function_info) {
HandleScope scope(isolate());
Handle<CompilationCacheTable> table = GetFirstTable();
SetFirstTable(CompilationCacheTable::PutScript(table, source, language_mode,
function_info, isolate()));
SetFirstTable(CompilationCacheTable::PutScript(table, source, function_info,
isolate()));
}
InfoCellPair CompilationCacheEval::Lookup(Handle<String> source,
......@@ -293,11 +353,11 @@ void CompilationCache::Remove(Handle<SharedFunctionInfo> function_info) {
script_.Remove(function_info);
}
MaybeHandle<SharedFunctionInfo> CompilationCache::LookupScript(
CompilationCacheScript::LookupResult CompilationCache::LookupScript(
Handle<String> source, const ScriptDetails& script_details,
LanguageMode language_mode) {
if (!IsEnabledScriptAndEval()) return MaybeHandle<SharedFunctionInfo>();
return script_.Lookup(source, script_details, language_mode);
if (!IsEnabledScript(language_mode)) return {};
return script_.Lookup(source, script_details);
}
InfoCellPair CompilationCache::LookupEval(Handle<String> source,
......@@ -338,10 +398,10 @@ MaybeHandle<FixedArray> CompilationCache::LookupRegExp(Handle<String> source,
void CompilationCache::PutScript(Handle<String> source,
LanguageMode language_mode,
Handle<SharedFunctionInfo> function_info) {
if (!IsEnabledScriptAndEval()) return;
if (!IsEnabledScript(language_mode)) return;
LOG(isolate(), CompilationCacheEvent("put", "script", *function_info));
script_.Put(source, language_mode, function_info);
script_.Put(source, function_info);
}
void CompilationCache::PutEval(Handle<String> source,
......
......@@ -65,9 +65,11 @@ class CompilationSubCache {
Isolate* isolate() const { return isolate_; }
// Ageing occurs either by removing the oldest generation, or with
// custom logic implemented in CompilationCacheTable::Age.
// custom subclass logic.
static void AgeByGeneration(CompilationSubCache* c);
static void AgeCustom(CompilationSubCache* c);
// Returns null if there is no first table.
CompilationCacheTable MaybeGetFirstTable();
private:
Isolate* const isolate_;
......@@ -82,12 +84,11 @@ class CompilationCacheScript : public CompilationSubCache {
public:
explicit CompilationCacheScript(Isolate* isolate);
MaybeHandle<SharedFunctionInfo> Lookup(Handle<String> source,
const ScriptDetails& script_details,
LanguageMode language_mode);
using LookupResult = CompilationCacheScriptLookupResult;
LookupResult Lookup(Handle<String> source,
const ScriptDetails& script_details);
void Put(Handle<String> source, LanguageMode language_mode,
Handle<SharedFunctionInfo> function_info);
void Put(Handle<String> source, Handle<SharedFunctionInfo> function_info);
void Age() override;
......@@ -154,10 +155,10 @@ class V8_EXPORT_PRIVATE CompilationCache {
CompilationCache(const CompilationCache&) = delete;
CompilationCache& operator=(const CompilationCache&) = delete;
// Finds the script shared function info for a source
// string. Returns an empty handle if the cache doesn't contain a
// script for the given source string with the right origin.
MaybeHandle<SharedFunctionInfo> LookupScript(
// Finds the Script and root SharedFunctionInfo for a script source string.
// Returns empty handles if the cache doesn't contain a script for the given
// source string with the right origin.
CompilationCacheScript::LookupResult LookupScript(
Handle<String> source, const ScriptDetails& script_details,
LanguageMode language_mode);
......@@ -174,7 +175,7 @@ class V8_EXPORT_PRIVATE CompilationCache {
MaybeHandle<FixedArray> LookupRegExp(Handle<String> source,
JSRegExp::Flags flags);
// Associate the (source, kind) pair to the shared function
// Associate the source string to the shared function
// info. This may overwrite an existing mapping.
void PutScript(Handle<String> source, LanguageMode language_mode,
Handle<SharedFunctionInfo> function_info);
......@@ -224,6 +225,11 @@ class V8_EXPORT_PRIVATE CompilationCache {
bool IsEnabledScriptAndEval() const {
return FLAG_compilation_cache && enabled_script_and_eval_;
}
bool IsEnabledScript(LanguageMode language_mode) {
// Tests can change FLAG_use_strict at runtime. The compilation cache only
// contains scripts which were compiled with the default language mode.
return IsEnabledScriptAndEval() && language_mode == LanguageMode::kSloppy;
}
Isolate* isolate() const { return isolate_; }
......
......@@ -1430,6 +1430,19 @@ Handle<SharedFunctionInfo> CreateTopLevelSharedFunctionInfo(
parse_info->literal(), script, true);
}
Handle<SharedFunctionInfo> GetOrCreateTopLevelSharedFunctionInfo(
ParseInfo* parse_info, Handle<Script> script, Isolate* isolate) {
EnsureSharedFunctionInfosArrayOnScript(script, parse_info, isolate);
MaybeHandle<SharedFunctionInfo> maybe_shared =
Script::FindSharedFunctionInfo(script, isolate, parse_info->literal());
if (Handle<SharedFunctionInfo> shared; maybe_shared.ToHandle(&shared)) {
DCHECK_EQ(shared->function_literal_id(),
parse_info->literal()->function_literal_id());
return shared;
}
return CreateTopLevelSharedFunctionInfo(parse_info, script, isolate);
}
MaybeHandle<SharedFunctionInfo> CompileToplevel(
ParseInfo* parse_info, Handle<Script> script,
MaybeHandle<ScopeInfo> maybe_outer_scope_info, Isolate* isolate,
......@@ -1463,7 +1476,7 @@ MaybeHandle<SharedFunctionInfo> CompileToplevel(
// Create the SharedFunctionInfo and add it to the script's list.
Handle<SharedFunctionInfo> shared_info =
CreateTopLevelSharedFunctionInfo(parse_info, script, isolate);
GetOrCreateTopLevelSharedFunctionInfo(parse_info, script, isolate);
FinalizeUnoptimizedCompilationDataList
finalize_unoptimized_compilation_data_list;
......@@ -2806,17 +2819,22 @@ Handle<Script> NewScript(
}
MaybeHandle<SharedFunctionInfo> CompileScriptOnMainThread(
const UnoptimizedCompileFlags flags, Handle<String> source,
UnoptimizedCompileFlags flags, Handle<String> source,
const ScriptDetails& script_details, NativesFlag natives,
v8::Extension* extension, Isolate* isolate,
IsCompiledScope* is_compiled_scope) {
MaybeHandle<Script> maybe_script, IsCompiledScope* is_compiled_scope) {
Handle<Script> script;
if (maybe_script.ToHandle(&script)) {
flags.set_script_id(script->id());
}
UnoptimizedCompileState compile_state;
ReusableUnoptimizedCompileState reusable_state(isolate);
ParseInfo parse_info(isolate, flags, &compile_state, &reusable_state);
parse_info.set_extension(extension);
Handle<Script> script =
NewScript(isolate, &parse_info, source, script_details, natives);
if (script.is_null()) {
script = NewScript(isolate, &parse_info, source, script_details, natives);
}
DCHECK_IMPLIES(parse_info.flags().collect_type_profile(),
script->IsUserJavaScript());
DCHECK_EQ(parse_info.flags().is_repl_mode(), script->is_repl_mode());
......@@ -2896,8 +2914,10 @@ bool CompilationExceptionIsRangeError(Isolate* isolate, Handle<Object> obj) {
MaybeHandle<SharedFunctionInfo> CompileScriptOnBothBackgroundAndMainThread(
Handle<String> source, const ScriptDetails& script_details,
Isolate* isolate, IsCompiledScope* is_compiled_scope) {
MaybeHandle<Script> maybe_script, Isolate* isolate,
IsCompiledScope* is_compiled_scope) {
// Start a background thread compiling the script.
// TODO(v8:12808): Use maybe_script for the background compilation.
StressBackgroundCompileThread background_compile_thread(
isolate, source,
script_details.origin_options.IsModule() ? ScriptType::kModule
......@@ -2920,7 +2940,7 @@ MaybeHandle<SharedFunctionInfo> CompileScriptOnBothBackgroundAndMainThread(
flags_copy.set_script_id(Script::kTemporaryScriptId);
main_thread_maybe_result = CompileScriptOnMainThread(
flags_copy, source, script_details, NOT_NATIVES_CODE, nullptr, isolate,
&inner_is_compiled_scope);
MaybeHandle<Script>(), &inner_is_compiled_scope);
if (main_thread_maybe_result.is_null()) {
// Assume all range errors are stack overflows.
main_thread_had_stack_overflow = CompilationExceptionIsRangeError(
......@@ -3011,6 +3031,7 @@ MaybeHandle<SharedFunctionInfo> GetSharedFunctionInfoForScriptImpl(
const bool use_compilation_cache =
extension == nullptr && script_details.repl_mode == REPLMode::kNo;
MaybeHandle<SharedFunctionInfo> maybe_result;
MaybeHandle<Script> maybe_script;
IsCompiledScope is_compiled_scope;
if (use_compilation_cache) {
bool can_consume_code_cache =
......@@ -3020,8 +3041,11 @@ MaybeHandle<SharedFunctionInfo> GetSharedFunctionInfoForScriptImpl(
}
// First check per-isolate compilation cache.
maybe_result =
CompilationCacheScript::LookupResult lookup_result =
compilation_cache->LookupScript(source, script_details, language_mode);
maybe_script = lookup_result.script();
maybe_result = lookup_result.toplevel_sfi();
is_compiled_scope = lookup_result.is_compiled_scope();
if (!maybe_result.is_null()) {
compile_timer.set_hit_isolate_cache();
} else if (can_consume_code_cache) {
......@@ -3032,6 +3056,10 @@ MaybeHandle<SharedFunctionInfo> GetSharedFunctionInfoForScriptImpl(
RCS_SCOPE(isolate, RuntimeCallCounterId::kCompileDeserialize);
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
"V8.CompileDeserialize");
// TODO(v8:12808): If a Script was found in the compilation cache, then
// both of the code paths below (Finish and Deserialize) should make use
// of that Script to avoid duplicating the Script itself or any
// preexisting SharedFunctionInfos.
if (deserialize_task) {
// If there's a cache consume task, finish it.
maybe_result = deserialize_task->Finish(isolate, source,
......@@ -3066,7 +3094,7 @@ MaybeHandle<SharedFunctionInfo> GetSharedFunctionInfoForScriptImpl(
// If the --stress-background-compile flag is set, do the actual
// compilation on a background thread, and wait for its result.
maybe_result = CompileScriptOnBothBackgroundAndMainThread(
source, script_details, isolate, &is_compiled_scope);
source, script_details, maybe_script, isolate, &is_compiled_scope);
} else {
UnoptimizedCompileFlags flags =
UnoptimizedCompileFlags::ForToplevelCompile(
......@@ -3078,9 +3106,9 @@ MaybeHandle<SharedFunctionInfo> GetSharedFunctionInfoForScriptImpl(
flags.set_is_eager(compile_options == ScriptCompiler::kEagerCompile);
maybe_result =
CompileScriptOnMainThread(flags, source, script_details, natives,
extension, isolate, &is_compiled_scope);
maybe_result = CompileScriptOnMainThread(
flags, source, script_details, natives, extension, isolate,
maybe_script, &is_compiled_scope);
}
// Add the result to the isolate cache.
......@@ -3253,8 +3281,18 @@ Compiler::GetSharedFunctionInfoForStreamedScript(
{
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
"V8.StreamingFinalization.CheckCache");
maybe_result = compilation_cache->LookupScript(
source, script_details, task->flags().outer_language_mode());
CompilationCacheScript::LookupResult lookup_result =
compilation_cache->LookupScript(source, script_details,
task->flags().outer_language_mode());
// TODO(v8:12808): Determine what to do if we finish streaming and find that
// another copy of the Script already exists but has no root
// SharedFunctionInfo or has an uncompiled SharedFunctionInfo. For now, we
// just ignore it and create a new Script.
if (!lookup_result.toplevel_sfi().is_null()) {
maybe_result = lookup_result.toplevel_sfi();
}
if (!maybe_result.is_null()) {
compile_timer.set_hit_isolate_cache();
}
......@@ -3275,6 +3313,8 @@ Compiler::GetSharedFunctionInfoForStreamedScript(
// Add compiled code to the isolate cache.
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
"V8.StreamingFinalization.AddToCache");
DCHECK_EQ(task->flags().outer_language_mode(),
construct_language_mode(FLAG_use_strict));
compilation_cache->PutScript(source, task->flags().outer_language_mode(),
result);
}
......
......@@ -3899,6 +3899,12 @@ class BigIntPlatform : public bigint::Platform {
private:
Isolate* isolate_;
};
void MarkCompactPrologue(v8::Isolate* v8_isolate, v8::GCType gc_type,
v8::GCCallbackFlags flags, void* data) {
Isolate* isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
isolate->compilation_cache()->MarkCompactPrologue();
}
} // namespace
VirtualMemoryCage* Isolate::GetPtrComprCodeCageForTesting() {
......@@ -4256,6 +4262,9 @@ bool Isolate::Init(SnapshotData* startup_snapshot_data,
}
#endif
heap()->AddGCPrologueCallback(MarkCompactPrologue, kGCTypeMarkSweepCompact,
nullptr);
initialized_ = true;
return true;
......
......@@ -2035,11 +2035,6 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory {
static void SetIsolateThreadLocals(Isolate* isolate,
PerIsolateThreadData* data);
void MarkCompactPrologue(bool is_compacting,
ThreadLocalTop* archived_thread_data);
void MarkCompactEpilogue(bool is_compacting,
ThreadLocalTop* archived_thread_data);
void FillCache();
// Propagate pending exception message to the v8::TryCatch.
......
......@@ -2669,8 +2669,6 @@ void Heap::MarkCompactPrologue() {
RegExpResultsCache::Clear(string_split_cache());
RegExpResultsCache::Clear(regexp_multiple_cache());
isolate_->compilation_cache()->MarkCompactPrologue();
FlushNumberStringCache();
}
......
......@@ -287,6 +287,9 @@ namespace internal {
SC(alive_after_last_gc, V8.AliveAfterLastGC) \
SC(compilation_cache_hits, V8.CompilationCacheHits) \
SC(compilation_cache_misses, V8.CompilationCacheMisses) \
/* Number of times the cache contained a reusable Script but not \
the root SharedFunctionInfo */ \
SC(compilation_cache_partial_hits, V8.CompilationCachePartialHits) \
SC(objs_since_last_young, V8.ObjsSinceLastYoung) \
SC(objs_since_last_full, V8.ObjsSinceLastFull)
......
......@@ -26,11 +26,73 @@ CompilationCacheTable::CompilationCacheTable(Address ptr)
NEVER_READ_ONLY_SPACE_IMPL(CompilationCacheTable)
CAST_ACCESSOR(CompilationCacheTable)
Object CompilationCacheTable::PrimaryValueAt(InternalIndex entry) {
return get(EntryToIndex(entry) + 1);
}
void CompilationCacheTable::SetPrimaryValueAt(InternalIndex entry, Object value,
WriteBarrierMode mode) {
set(EntryToIndex(entry) + 1, value, mode);
}
Object CompilationCacheTable::EvalFeedbackValueAt(InternalIndex entry) {
STATIC_ASSERT(CompilationCacheShape::kEntrySize == 3);
return get(EntryToIndex(entry) + 2);
}
void CompilationCacheTable::SetEvalFeedbackValueAt(InternalIndex entry,
Object value,
WriteBarrierMode mode) {
set(EntryToIndex(entry) + 2, value, mode);
}
// The key in a script cache is a WeakFixedArray containing a weak pointer to
// the Script. The corresponding value can be either the root SharedFunctionInfo
// or undefined. The purpose of storing the root SharedFunctionInfo as the value
// is to keep it alive, not to save a lookup on the Script. A newly added entry
// always contains the root SharedFunctionInfo. After the root
// SharedFunctionInfo has aged sufficiently, it is replaced with undefined. In
// this way, all strong references to large objects are dropped, but there is
// still a way to get the Script if it happens to still be alive.
class ScriptCacheKey : public HashTableKey {
public:
enum Index {
kWeakScript,
kEnd,
};
explicit ScriptCacheKey(Handle<String> source);
bool IsMatch(Object other) override;
Handle<Object> AsHandle(Isolate* isolate, Handle<SharedFunctionInfo> shared);
static base::Optional<String> SourceFromObject(Object obj) {
DisallowGarbageCollection no_gc;
DCHECK(obj.IsWeakFixedArray());
WeakFixedArray array = WeakFixedArray::cast(obj);
DCHECK_EQ(array.length(), kEnd);
MaybeObject maybe_script = array.Get(kWeakScript);
if (HeapObject script; maybe_script.GetHeapObjectIfWeak(&script)) {
PrimitiveHeapObject source_or_undefined = Script::cast(script).source();
// Scripts stored in the script cache should always have a source string.
return String::cast(source_or_undefined);
}
DCHECK(maybe_script.IsCleared());
return {};
}
private:
Handle<String> source_;
};
uint32_t CompilationCacheShape::RegExpHash(String string, Smi flags) {
return string.EnsureHash() + flags.value();
}
uint32_t CompilationCacheShape::StringSharedHash(String source,
uint32_t CompilationCacheShape::EvalHash(String source,
SharedFunctionInfo shared,
LanguageMode language_mode,
int position) {
......@@ -50,11 +112,8 @@ uint32_t CompilationCacheShape::StringSharedHash(String source,
return hash;
}
uint32_t CompilationCacheShape::StringSharedHash(String source,
LanguageMode language_mode) {
uint32_t CompilationCacheShape::ScriptHash(String source) {
uint32_t hash = source.EnsureHash();
STATIC_ASSERT(LanguageModeSize == 2);
if (is_strict(language_mode)) hash ^= 0x8000;
return hash;
}
......@@ -68,7 +127,17 @@ uint32_t CompilationCacheShape::HashForObject(ReadOnlyRoots roots,
return SharedFunctionInfo::cast(object).Hash();
}
// Script: See StringSharedKey::ToHandle for the encoding.
// Script.
if (object.IsWeakFixedArray()) {
base::Optional<String> script_key =
ScriptCacheKey::SourceFromObject(object);
// Rehashing should only happen after we've removed all of the keys
// containing cleared weak refs.
DCHECK(script_key);
return ScriptHash(*script_key);
}
// Eval: See EvalCacheKey::ToHandle for the encoding.
FixedArray val = FixedArray::cast(object);
if (val.map() == roots.fixed_cow_array_map()) {
DCHECK_EQ(4, val.length());
......@@ -77,14 +146,10 @@ uint32_t CompilationCacheShape::HashForObject(ReadOnlyRoots roots,
DCHECK(is_valid_language_mode(language_unchecked));
LanguageMode language_mode = static_cast<LanguageMode>(language_unchecked);
int position = Smi::ToInt(val.get(3));
Object shared_or_smi = val.get(0);
if (shared_or_smi.IsSmi()) {
DCHECK_EQ(position, kNoSourcePosition);
return StringSharedHash(source, language_mode);
} else {
return StringSharedHash(source, SharedFunctionInfo::cast(shared_or_smi),
language_mode, position);
}
Object shared = val.get(0);
DCHECK(shared.IsSharedFunctionInfo());
return EvalHash(source, SharedFunctionInfo::cast(shared), language_mode,
position);
}
// RegExp: The key field (and the value field) contains the
......
This diff is collapsed.
......@@ -29,13 +29,10 @@ class CompilationCacheShape : public BaseShape<HashTableKey*> {
static inline uint32_t RegExpHash(String string, Smi flags);
static inline uint32_t StringSharedHash(String source,
SharedFunctionInfo shared,
LanguageMode language_mode,
int position);
static inline uint32_t EvalHash(String source, SharedFunctionInfo shared,
LanguageMode language_mode, int position);
static inline uint32_t StringSharedHash(String source,
LanguageMode language_mode);
static inline uint32_t ScriptHash(String source);
static inline uint32_t HashForObject(ReadOnlyRoots roots, Object object);
......@@ -79,6 +76,34 @@ class InfoCellPair {
FeedbackCell feedback_cell_;
};
// A lookup result from the compilation cache for scripts. There are three
// possible states:
//
// 1. Cache miss: script and toplevel_sfi are both null.
// 2. Cache hit: script and toplevel_sfi are both non-null. toplevel_sfi is
// guaranteed to be compiled, and to stay compiled while this lookup result
// instance is alive.
// 3. Partial cache hit: script is non-null, but toplevel_sfi is null. The
// script may contain an uncompiled toplevel SharedFunctionInfo.
class CompilationCacheScriptLookupResult {
public:
MaybeHandle<Script> script() const { return script_; }
MaybeHandle<SharedFunctionInfo> toplevel_sfi() const { return toplevel_sfi_; }
IsCompiledScope is_compiled_scope() const { return is_compiled_scope_; }
using RawObjects = std::pair<Script, SharedFunctionInfo>;
RawObjects GetRawObjects() const;
static CompilationCacheScriptLookupResult FromRawObjects(RawObjects raw,
Isolate* isolate);
private:
MaybeHandle<Script> script_;
MaybeHandle<SharedFunctionInfo> toplevel_sfi_;
IsCompiledScope is_compiled_scope_;
};
EXTERN_DECLARE_HASH_TABLE(CompilationCacheTable, CompilationCacheShape)
class CompilationCacheTable
......@@ -86,14 +111,18 @@ class CompilationCacheTable
public:
NEVER_READ_ONLY_SPACE
// The 'script' cache contains SharedFunctionInfos.
static MaybeHandle<SharedFunctionInfo> LookupScript(
// The 'script' cache contains SharedFunctionInfos. Once a root
// SharedFunctionInfo has become old enough that its bytecode is flushed, the
// entry is still present and can be used to get the Script. For consistency,
// this function always returns the Script. Callers should check whether there
// is a root SharedFunctionInfo in the script and whether it is already
// compiled, and choose what to do accordingly.
static CompilationCacheScriptLookupResult LookupScript(
Handle<CompilationCacheTable> table, Handle<String> src,
LanguageMode language_mode, Isolate* isolate);
Isolate* isolate);
static Handle<CompilationCacheTable> PutScript(
Handle<CompilationCacheTable> cache, Handle<String> src,
LanguageMode language_mode, Handle<SharedFunctionInfo> value,
Isolate* isolate);
Handle<SharedFunctionInfo> value, Isolate* isolate);
// Eval code only gets cached after a second probe for the
// code object. To do so, on first "put" only a hash identifying the
......@@ -124,12 +153,24 @@ class CompilationCacheTable
JSRegExp::Flags flags, Handle<FixedArray> value);
void Remove(Object value);
void Age(Isolate* isolate);
void RemoveEntry(InternalIndex entry);
inline Object PrimaryValueAt(InternalIndex entry);
inline void SetPrimaryValueAt(InternalIndex entry, Object value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
inline Object EvalFeedbackValueAt(InternalIndex entry);
inline void SetEvalFeedbackValueAt(
InternalIndex entry, Object value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
// The initial placeholder insertion of the eval cache survives this many GCs.
static constexpr int kHashGenerations = 10;
DECL_CAST(CompilationCacheTable)
private:
void RemoveEntry(int entry_index);
static Handle<CompilationCacheTable> EnsureScriptTableCapacity(
Isolate* isolate, Handle<CompilationCacheTable> cache);
OBJECT_CONSTRUCTORS(CompilationCacheTable,
HashTable<CompilationCacheTable, CompilationCacheShape>);
......
......@@ -238,6 +238,12 @@ Object HashTable<Derived, Shape>::KeyAt(PtrComprCageBase cage_base,
return get(cage_base, EntryToIndex(entry) + kEntryKeyIndex, tag);
}
template <typename Derived, typename Shape>
void HashTable<Derived, Shape>::SetKeyAt(InternalIndex entry, Object value,
WriteBarrierMode mode) {
set_key(EntryToIndex(entry), value, mode);
}
template <typename Derived, typename Shape>
void HashTable<Derived, Shape>::set_key(int index, Object value) {
DCHECK(!IsEphemeronHashTable());
......
......@@ -165,6 +165,9 @@ class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE) HashTable
inline Object KeyAt(PtrComprCageBase cage_base, InternalIndex entry,
RelaxedLoadTag tag);
inline void SetKeyAt(InternalIndex entry, Object value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
static const int kElementsStartIndex = kPrefixStartIndex + Shape::kPrefixSize;
static const int kEntrySize = Shape::kEntrySize;
STATIC_ASSERT(kEntrySize > 0);
......
This diff is collapsed.
......@@ -23843,23 +23843,25 @@ TEST(StreamingWithHarmonyScopes) {
delete[] full_source;
}
// Regression test for crbug.com/v8/12668. Verifies that after a streamed script
// is inserted into the isolate script cache, a non-streamed script with
// identical origin can reuse that data.
TEST(StreamingWithIsolateScriptCache) {
namespace {
void StreamingWithIsolateScriptCache(bool run_gc) {
i::FLAG_expose_gc = true;
const char* chunks[] = {"'use strict'; (function test() { return 13; })",
nullptr};
const char* full_source = chunks[0];
v8::Isolate* isolate = CcTest::isolate();
auto i_isolate = reinterpret_cast<i::Isolate*>(isolate);
v8::HandleScope scope(isolate);
v8::ScriptOrigin origin(isolate, v8_str("http://foo.com"), 0, 0, false, -1,
v8::Local<v8::Value>(), false, false, false);
v8::Local<Value> first_function_untyped;
i::Handle<i::JSFunction> first_function;
i::Handle<i::JSFunction> second_function;
// Run the script using streaming.
{
LocalContext env;
v8::EscapableHandleScope inner_scope(isolate);
v8::ScriptCompiler::StreamedSource source(
std::make_unique<TestSourceStream>(chunks),
......@@ -23874,13 +23876,38 @@ TEST(StreamingWithIsolateScriptCache) {
origin)
.ToLocalChecked();
v8::Local<Value> result(script->Run(env.local()).ToLocalChecked());
first_function =
i::Handle<i::JSFunction>::cast(v8::Utils::OpenHandle(*result));
first_function_untyped = inner_scope.Escape(result);
if (run_gc) {
// Age the top-level bytecode for the script to encourage the Isolate
// script cache to evict it. However, there are still active Handles
// referring to functions in that script, so the script itself should stay
// alive and reachable via the Isolate script cache.
i::Handle<i::JSFunction> script_function =
i::Handle<i::JSFunction>::cast(v8::Utils::OpenHandle(*script));
i::Handle<i::BytecodeArray> script_bytecode(
script_function->shared().GetBytecodeArray(i_isolate), i_isolate);
for (int i = 0; i < 5; ++i) {
script_bytecode->MakeOlder();
}
}
}
first_function = i::Handle<i::JSFunction>::cast(
v8::Utils::OpenHandle(*first_function_untyped));
// Run the same script in another Context without streaming.
{
LocalContext env;
if (run_gc) {
// Perform garbage collection, which should remove the top-level
// SharedFunctionInfo from the Isolate script cache. However, the
// corresponding Script is still reachable and therefore still present in
// the Isolate script cache.
CompileRun("gc();");
}
v8::ScriptCompiler::Source script_source(v8_str(full_source), origin);
Local<Script> script =
v8::ScriptCompiler::Compile(env.local(), &script_source)
......@@ -23894,6 +23921,26 @@ TEST(StreamingWithIsolateScriptCache) {
// SharedFunctionInfo instance due to the isolate script cache.
CHECK_EQ(first_function->shared(), second_function->shared());
}
} // namespace
// Regression test for crbug.com/v8/12668. Verifies that after a streamed script
// is inserted into the isolate script cache, a non-streamed script with
// identical origin can reuse that data.
TEST(StreamingWithIsolateScriptCache) {
StreamingWithIsolateScriptCache(false);
}
// Variant of the above test which evicts the root SharedFunctionInfo from the
// Isolate script cache but still reuses the same Script.
TEST(StreamingWithIsolateScriptCacheClearingRootSFI) {
// TODO(v8:12808): Remove this check once background compilation is capable of
// reusing an existing Script.
if (v8::internal::FLAG_stress_background_compile) {
return;
}
StreamingWithIsolateScriptCache(true);
}
TEST(CodeCache) {
v8::Isolate::CreateParams create_params;
......@@ -1820,10 +1820,9 @@ TEST(CodeSerializerPromotedToCompilationCache) {
ScriptDetails script_details(src);
script_details.host_defined_options =
default_script_details.host_defined_options;
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK_EQ(*shared.ToHandleChecked(), *copy);
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK_EQ(*lookup_result.toplevel_sfi().ToHandleChecked(), *copy);
}
{
......@@ -1837,10 +1836,9 @@ TEST(CodeSerializerPromotedToCompilationCache) {
default_host_defined_option_1_string);
host_defined_options->set(1, *host_defined_option_1);
script_details.host_defined_options = host_defined_options;
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK_EQ(*shared.ToHandleChecked(), *copy);
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK_EQ(*lookup_result.toplevel_sfi().ToHandleChecked(), *copy);
}
{
......@@ -1849,49 +1847,48 @@ TEST(CodeSerializerPromotedToCompilationCache) {
isolate->factory()->NewStringFromAsciiChecked(source));
script_details.host_defined_options =
default_script_details.host_defined_options;
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK_EQ(*shared.ToHandleChecked(), *copy);
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK_EQ(*lookup_result.toplevel_sfi().ToHandleChecked(), *copy);
}
{
// Lookup with different name string should fail:
ScriptDetails script_details(
isolate->factory()->NewStringFromAsciiChecked("other"));
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
{
// Lookup with different position should fail:
ScriptDetails script_details(src);
script_details.line_offset = 0xFF;
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
{
// Lookup with different position should fail:
ScriptDetails script_details(src);
script_details.column_offset = 0xFF;
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
{
// Lookup with different language mode should fail:
ScriptDetails script_details(src);
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kStrict);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kStrict);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
{
......@@ -1899,20 +1896,20 @@ TEST(CodeSerializerPromotedToCompilationCache) {
ScriptOriginOptions origin_options(false, true);
CHECK_NE(ScriptOriginOptions().Flags(), origin_options.Flags());
ScriptDetails script_details(src, origin_options);
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
{
// Lookup with different host_defined_options should fail:
ScriptDetails script_details(src);
script_details.host_defined_options = isolate->factory()->NewFixedArray(5);
MaybeHandle<SharedFunctionInfo> shared =
isolate->compilation_cache()->LookupScript(src, script_details,
LanguageMode::kSloppy);
CHECK(shared.is_null());
auto lookup_result = isolate->compilation_cache()->LookupScript(
src, script_details, LanguageMode::kSloppy);
CHECK(lookup_result.script().is_null() &&
lookup_result.toplevel_sfi().is_null());
}
delete cache;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment