blob: 5ebd09189db1505276ea4875b2b1336b76268612 [file] [log] [blame]
// Copyright 2020 the V8 project 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 "src/objects/compilation-cache-table.h"
#include "src/codegen/script-details.h"
#include "src/common/assert-scope.h"
#include "src/objects/compilation-cache-table-inl.h"
namespace v8 {
namespace internal {
namespace {
const int kLiteralEntryLength = 2;
const int kLiteralInitialLength = 2;
const int kLiteralContextOffset = 0;
const int kLiteralLiteralsOffset = 1;
int SearchLiteralsMapEntry(Tagged<CompilationCacheTable> cache,
InternalIndex cache_entry,
Tagged<Context> native_context) {
DisallowGarbageCollection no_gc;
DCHECK(IsNativeContext(native_context));
Tagged<Object> obj = cache->EvalFeedbackValueAt(cache_entry);
// Check that there's no confusion between FixedArray and WeakFixedArray (the
// object used to be a FixedArray here).
DCHECK(!IsFixedArray(obj));
if (IsWeakFixedArray(obj)) {
Tagged<WeakFixedArray> literals_map = WeakFixedArray::cast(obj);
int length = literals_map->length();
for (int i = 0; i < length; i += kLiteralEntryLength) {
DCHECK(literals_map->get(i + kLiteralContextOffset).IsWeakOrCleared());
if (literals_map->get(i + kLiteralContextOffset) ==
MakeWeak(native_context)) {
return i;
}
}
}
return -1;
}
void AddToFeedbackCellsMap(Handle<CompilationCacheTable> cache,
InternalIndex cache_entry,
Handle<Context> native_context,
Handle<FeedbackCell> feedback_cell) {
Isolate* isolate = native_context->GetIsolate();
DCHECK(IsNativeContext(*native_context));
static_assert(kLiteralEntryLength == 2);
Handle<WeakFixedArray> new_literals_map;
int entry;
Tagged<Object> obj = cache->EvalFeedbackValueAt(cache_entry);
// Check that there's no confusion between FixedArray and WeakFixedArray (the
// object used to be a FixedArray here).
DCHECK(!IsFixedArray(obj));
if (!IsWeakFixedArray(obj) || WeakFixedArray::cast(obj)->length() == 0) {
new_literals_map = isolate->factory()->NewWeakFixedArray(
kLiteralInitialLength, AllocationType::kOld);
entry = 0;
} else {
Handle<WeakFixedArray> old_literals_map(WeakFixedArray::cast(obj), isolate);
entry = SearchLiteralsMapEntry(*cache, cache_entry, *native_context);
if (entry >= 0) {
// Just set the code of the entry.
old_literals_map->set(entry + kLiteralLiteralsOffset,
MakeWeak(*feedback_cell));
return;
}
// Can we reuse an entry?
DCHECK_LT(entry, 0);
int length = old_literals_map->length();
for (int i = 0; i < length; i += kLiteralEntryLength) {
if (old_literals_map->get(i + kLiteralContextOffset).IsCleared()) {
new_literals_map = old_literals_map;
entry = i;
break;
}
}
if (entry < 0) {
// Copy old optimized code map and append one new entry.
new_literals_map = isolate->factory()->CopyWeakFixedArrayAndGrow(
old_literals_map, kLiteralEntryLength);
entry = old_literals_map->length();
}
}
new_literals_map->set(entry + kLiteralContextOffset,
MakeWeak(*native_context));
new_literals_map->set(entry + kLiteralLiteralsOffset,
MakeWeak(*feedback_cell));
#ifdef DEBUG
for (int i = 0; i < new_literals_map->length(); i += kLiteralEntryLength) {
Tagged<MaybeObject> object =
new_literals_map->get(i + kLiteralContextOffset);
DCHECK(object.IsCleared() ||
IsNativeContext(object.GetHeapObjectAssumeWeak()));
object = new_literals_map->get(i + kLiteralLiteralsOffset);
DCHECK(object.IsCleared() ||
IsFeedbackCell(object.GetHeapObjectAssumeWeak()));
}
#endif
Tagged<Object> old_literals_map = cache->EvalFeedbackValueAt(cache_entry);
if (old_literals_map != *new_literals_map) {
cache->SetEvalFeedbackValueAt(cache_entry, *new_literals_map);
}
}
Tagged<FeedbackCell> SearchLiteralsMap(Tagged<CompilationCacheTable> cache,
InternalIndex cache_entry,
Tagged<Context> native_context) {
Tagged<FeedbackCell> result;
int entry = SearchLiteralsMapEntry(cache, cache_entry, native_context);
if (entry >= 0) {
Tagged<WeakFixedArray> literals_map =
WeakFixedArray::cast(cache->EvalFeedbackValueAt(cache_entry));
DCHECK_LE(entry + kLiteralEntryLength, literals_map->length());
Tagged<MaybeObject> object =
literals_map->get(entry + kLiteralLiteralsOffset);
if (!object.IsCleared()) {
result = FeedbackCell::cast(object.GetHeapObjectAssumeWeak());
}
}
DCHECK(result.is_null() || IsFeedbackCell(result));
return result;
}
// EvalCacheKeys are used as keys in the eval cache.
class EvalCacheKey : public HashTableKey {
public:
// This tuple unambiguously identifies calls to eval() or
// CreateDynamicFunction() (such as through the Function() constructor).
// * source is the string passed into eval(). For dynamic functions, this is
// the effective source for the function, some of which is implicitly
// generated.
// * shared is the shared function info for the function containing the call
// to eval(). for dynamic functions, shared is the native context closure.
// * When positive, position is the position in the source where eval is
// called. When negative, position is the negation of the position in the
// dynamic function's effective source where the ')' ends the parameters.
EvalCacheKey(Handle<String> source, Handle<SharedFunctionInfo> shared,
LanguageMode language_mode, int position)
: HashTableKey(CompilationCacheShape::EvalHash(*source, *shared,
language_mode, position)),
source_(source),
shared_(shared),
language_mode_(language_mode),
position_(position) {}
bool IsMatch(Tagged<Object> other) override {
DisallowGarbageCollection no_gc;
if (!IsFixedArray(other)) {
DCHECK(IsNumber(other));
uint32_t other_hash = static_cast<uint32_t>(Object::Number(other));
return Hash() == other_hash;
}
Tagged<FixedArray> other_array = FixedArray::cast(other);
DCHECK(IsSharedFunctionInfo(other_array->get(0)));
if (*shared_ != other_array->get(0)) return false;
int language_unchecked = Smi::ToInt(other_array->get(2));
DCHECK(is_valid_language_mode(language_unchecked));
LanguageMode language_mode = static_cast<LanguageMode>(language_unchecked);
if (language_mode != language_mode_) return false;
int position = Smi::ToInt(other_array->get(3));
if (position != position_) return false;
Tagged<String> source = String::cast(other_array->get(1));
return source->Equals(*source_);
}
Handle<Object> AsHandle(Isolate* isolate) {
Handle<FixedArray> array = isolate->factory()->NewFixedArray(4);
array->set(0, *shared_);
array->set(1, *source_);
array->set(2, Smi::FromEnum(language_mode_));
array->set(3, Smi::FromInt(position_));
array->set_map(ReadOnlyRoots(isolate).fixed_cow_array_map());
return array;
}
private:
Handle<String> source_;
Handle<SharedFunctionInfo> shared_;
LanguageMode language_mode_;
int position_;
};
// RegExpKey carries the source and flags of a regular expression as key.
class RegExpKey : public HashTableKey {
public:
RegExpKey(Handle<String> string, JSRegExp::Flags flags)
: HashTableKey(
CompilationCacheShape::RegExpHash(*string, Smi::FromInt(flags))),
string_(string),
flags_(Smi::FromInt(flags)) {}
// Rather than storing the key in the hash table, a pointer to the
// stored value is stored where the key should be. IsMatch then
// compares the search key to the found object, rather than comparing
// a key to a key.
bool IsMatch(Tagged<Object> obj) override {
Tagged<FixedArray> val = FixedArray::cast(obj);
return string_->Equals(String::cast(val->get(JSRegExp::kSourceIndex))) &&
(flags_ == val->get(JSRegExp::kFlagsIndex));
}
Handle<String> string_;
Tagged<Smi> flags_;
};
// CodeKey carries the SharedFunctionInfo key associated with a
// Code object value.
class CodeKey : public HashTableKey {
public:
explicit CodeKey(Handle<SharedFunctionInfo> key)
: HashTableKey(key->Hash()), key_(key) {}
bool IsMatch(Tagged<Object> string) override { return *key_ == string; }
Handle<SharedFunctionInfo> key_;
};
Tagged<Smi> ScriptHash(Tagged<String> source, MaybeHandle<Object> maybe_name,
int line_offset, int column_offset,
v8::ScriptOriginOptions origin_options,
Isolate* isolate) {
DisallowGarbageCollection no_gc;
size_t hash = base::hash_combine(source->EnsureHash());
if (Handle<Object> name;
maybe_name.ToHandle(&name) && IsString(*name, isolate)) {
hash =
base::hash_combine(hash, String::cast(*name)->EnsureHash(), line_offset,
column_offset, origin_options.Flags());
}
// The upper bits of the hash are discarded so that the value fits in a Smi.
return Smi::From31BitPattern(static_cast<int>(hash & (~(1u << 31))));
}
} // 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 ScriptCacheKey::MatchesScript(Tagged<Script> script) {
DisallowGarbageCollection no_gc;
// If the script name isn't set, the boilerplate script should have
// an undefined name to have the same origin.
Handle<Object> name;
if (!name_.ToHandle(&name)) {
return IsUndefined(script->name(), isolate_);
}
// Do the fast bailout checks first.
if (line_offset_ != script->line_offset()) return false;
if (column_offset_ != script->column_offset()) return false;
// Check that both names are strings. If not, no match.
if (!IsString(*name, isolate_) || !IsString(script->name(), isolate_))
return false;
// Are the origin_options same?
if (origin_options_.Flags() != script->origin_options().Flags()) {
return false;
}
// Compare the two name strings for equality.
if (!String::cast(*name)->Equals(String::cast(script->name()))) {
return false;
}
Handle<FixedArray> wrapped_arguments_handle;
if (wrapped_arguments_.ToHandle(&wrapped_arguments_handle)) {
if (!script->is_wrapped()) {
return false;
}
Tagged<FixedArray> wrapped_arguments = *wrapped_arguments_handle;
Tagged<FixedArray> other_wrapped_arguments = script->wrapped_arguments();
int length = wrapped_arguments->length();
if (length != other_wrapped_arguments->length()) {
return false;
}
for (int i = 0; i < length; i++) {
Tagged<Object> arg = wrapped_arguments->get(i);
Tagged<Object> other_arg = other_wrapped_arguments->get(i);
DCHECK(IsString(arg));
DCHECK(IsString(other_arg));
if (!String::cast(arg)->Equals(String::cast(other_arg))) {
return false;
}
}
} else if (script->is_wrapped()) {
return false;
}
// Don't compare host options if the script was deserialized because we didn't
// serialize host options (see CodeSerializer::SerializeObjectImpl())
if (script->deserialized() &&
script->host_defined_options() ==
ReadOnlyRoots(isolate_).empty_fixed_array()) {
return true;
}
// TODO(cbruni, chromium:1244145): Remove once migrated to the context
Handle<Object> maybe_host_defined_options;
if (!host_defined_options_.ToHandle(&maybe_host_defined_options)) {
maybe_host_defined_options = isolate_->factory()->empty_fixed_array();
}
Tagged<FixedArray> host_defined_options =
FixedArray::cast(*maybe_host_defined_options);
Tagged<FixedArray> script_options =
FixedArray::cast(script->host_defined_options());
int length = host_defined_options->length();
if (length != script_options->length()) return false;
for (int i = 0; i < length; i++) {
// host-defined options is a v8::PrimitiveArray.
DCHECK(IsPrimitive(host_defined_options->get(i)));
DCHECK(IsPrimitive(script_options->get(i)));
if (!Object::StrictEquals(host_defined_options->get(i),
script_options->get(i))) {
return false;
}
}
return true;
}
ScriptCacheKey::ScriptCacheKey(Handle<String> source,
const ScriptDetails* script_details,
Isolate* isolate)
: ScriptCacheKey(source, script_details->name_obj,
script_details->line_offset, script_details->column_offset,
script_details->origin_options,
script_details->host_defined_options,
script_details->wrapped_arguments, isolate) {}
ScriptCacheKey::ScriptCacheKey(Handle<String> source, MaybeHandle<Object> name,
int line_offset, int column_offset,
v8::ScriptOriginOptions origin_options,
MaybeHandle<Object> host_defined_options,
MaybeHandle<FixedArray> maybe_wrapped_arguments,
Isolate* isolate)
: HashTableKey(static_cast<uint32_t>(ScriptHash(*source, name, line_offset,
column_offset,
origin_options, isolate)
.value())),
source_(source),
name_(name),
line_offset_(line_offset),
column_offset_(column_offset),
origin_options_(origin_options),
host_defined_options_(host_defined_options),
wrapped_arguments_(maybe_wrapped_arguments),
isolate_(isolate) {
DCHECK(Smi::IsValid(static_cast<int>(Hash())));
#ifdef DEBUG
Handle<FixedArray> wrapped_arguments;
if (maybe_wrapped_arguments.ToHandle(&wrapped_arguments)) {
int length = wrapped_arguments->length();
for (int i = 0; i < length; i++) {
Tagged<Object> arg = wrapped_arguments->get(i);
DCHECK(IsString(arg));
}
}
#endif
}
bool ScriptCacheKey::IsMatch(Tagged<Object> other) {
DisallowGarbageCollection no_gc;
DCHECK(IsWeakFixedArray(other));
Tagged<WeakFixedArray> other_array = WeakFixedArray::cast(other);
DCHECK_EQ(other_array->length(), kEnd);
// A hash check can quickly reject many non-matches, even though this step
// isn't strictly necessary.
uint32_t other_hash =
static_cast<uint32_t>(other_array->get(kHash).ToSmi().value());
if (other_hash != Hash()) return false;
Tagged<HeapObject> other_script_object;
if (!other_array->get(kWeakScript)
.GetHeapObjectIfWeak(&other_script_object)) {
return false;
}
Tagged<Script> other_script = Script::cast(other_script_object);
Tagged<String> other_source = String::cast(other_script->source());
return other_source->Equals(*source_) && MatchesScript(other_script);
}
Handle<Object> ScriptCacheKey::AsHandle(Isolate* isolate,
Handle<SharedFunctionInfo> shared) {
Handle<WeakFixedArray> array = isolate->factory()->NewWeakFixedArray(kEnd);
// Any SharedFunctionInfo being stored in the script cache should have a
// Script.
DCHECK(IsScript(shared->script()));
array->set(kHash, Smi::FromInt(static_cast<int>(Hash())));
array->set(kWeakScript, MakeWeak(shared->script()));
return array;
}
CompilationCacheScriptLookupResult::RawObjects
CompilationCacheScriptLookupResult::GetRawObjects() const {
RawObjects result;
if (Handle<Script> script; script_.ToHandle(&script)) {
result.first = *script;
}
if (Handle<SharedFunctionInfo> toplevel_sfi;
toplevel_sfi_.ToHandle(&toplevel_sfi)) {
result.second = *toplevel_sfi;
}
return result;
}
CompilationCacheScriptLookupResult
CompilationCacheScriptLookupResult::FromRawObjects(
CompilationCacheScriptLookupResult::RawObjects raw, Isolate* isolate) {
CompilationCacheScriptLookupResult result;
if (!raw.first.is_null()) {
result.script_ = handle(raw.first, isolate);
}
if (!raw.second.is_null()) {
result.is_compiled_scope_ = raw.second->is_compiled_scope(isolate);
if (result.is_compiled_scope_.is_compiled()) {
result.toplevel_sfi_ = handle(raw.second, isolate);
}
}
return result;
}
CompilationCacheScriptLookupResult CompilationCacheTable::LookupScript(
Handle<CompilationCacheTable> table, Handle<String> src,
const ScriptDetails& script_details, Isolate* isolate) {
src = String::Flatten(isolate, src);
ScriptCacheKey key(src, &script_details, isolate);
InternalIndex entry = table->FindEntry(isolate, &key);
if (entry.is_not_found()) return {};
DisallowGarbageCollection no_gc;
Tagged<Object> key_in_table = table->KeyAt(entry);
Tagged<Script> script = Script::cast(WeakFixedArray::cast(key_in_table)
->get(ScriptCacheKey::kWeakScript)
.GetHeapObjectAssumeWeak());
Tagged<Object> obj = table->PrimaryValueAt(entry);
Tagged<SharedFunctionInfo> toplevel_sfi;
if (!IsUndefined(obj, isolate)) {
toplevel_sfi = SharedFunctionInfo::cast(obj);
DCHECK_EQ(toplevel_sfi->script(), script);
}
return CompilationCacheScriptLookupResult::FromRawObjects(
std::make_pair(script, toplevel_sfi), isolate);
}
InfoCellPair CompilationCacheTable::LookupEval(
Handle<CompilationCacheTable> table, Handle<String> src,
Handle<SharedFunctionInfo> outer_info, Handle<Context> native_context,
LanguageMode language_mode, int position) {
InfoCellPair empty_result;
Isolate* isolate = native_context->GetIsolate();
src = String::Flatten(isolate, src);
EvalCacheKey key(src, outer_info, language_mode, position);
InternalIndex entry = table->FindEntry(isolate, &key);
if (entry.is_not_found()) return empty_result;
if (!IsFixedArray(table->KeyAt(entry))) return empty_result;
Tagged<Object> obj = table->PrimaryValueAt(entry);
if (!IsSharedFunctionInfo(obj)) return empty_result;
static_assert(CompilationCacheShape::kEntrySize == 3);
Tagged<FeedbackCell> feedback_cell =
SearchLiteralsMap(*table, entry, *native_context);
return InfoCellPair(isolate, SharedFunctionInfo::cast(obj), feedback_cell);
}
Handle<Object> CompilationCacheTable::LookupRegExp(Handle<String> src,
JSRegExp::Flags flags) {
Isolate* isolate = GetIsolate();
DisallowGarbageCollection no_gc;
RegExpKey key(src, flags);
InternalIndex entry = FindEntry(isolate, &key);
if (entry.is_not_found()) return isolate->factory()->undefined_value();
return Handle<Object>(PrimaryValueAt(entry), isolate);
}
Handle<CompilationCacheTable> CompilationCacheTable::EnsureScriptTableCapacity(
Isolate* isolate, Handle<CompilationCacheTable> cache) {
if (cache->HasSufficientCapacityToAdd(1)) return cache;
// Before resizing, delete are any entries whose keys contain cleared weak
// pointers.
{
DisallowGarbageCollection no_gc;
for (InternalIndex entry : cache->IterateEntries()) {
Tagged<Object> key;
if (!cache->ToKey(isolate, entry, &key)) continue;
if (WeakFixedArray::cast(key)
->get(ScriptCacheKey::kWeakScript)
.IsCleared()) {
DCHECK(IsUndefined(cache->PrimaryValueAt(entry)));
cache->RemoveEntry(entry);
}
}
}
return EnsureCapacity(isolate, cache);
}
Handle<CompilationCacheTable> CompilationCacheTable::PutScript(
Handle<CompilationCacheTable> cache, Handle<String> src,
MaybeHandle<FixedArray> maybe_wrapped_arguments,
Handle<SharedFunctionInfo> value, Isolate* isolate) {
src = String::Flatten(isolate, src);
Handle<Script> script = handle(Script::cast(value->script()), isolate);
MaybeHandle<Object> script_name;
if (IsString(script->name(), isolate)) {
script_name = handle(script->name(), isolate);
}
Handle<FixedArray> host_defined_options(script->host_defined_options(),
isolate);
ScriptCacheKey key(src, script_name, script->line_offset(),
script->column_offset(), script->origin_options(),
host_defined_options, maybe_wrapped_arguments, isolate);
Handle<Object> k = key.AsHandle(isolate, value);
// Check whether there is already a matching entry. If so, we must overwrite
// it. This allows an entry whose value is undefined to upgrade to contain a
// SharedFunctionInfo.
InternalIndex entry = cache->FindEntry(isolate, &key);
bool found_existing = entry.is_found();
if (!found_existing) {
cache = EnsureScriptTableCapacity(isolate, cache);
entry = cache->FindInsertionEntry(isolate, key.Hash());
}
// We might be tempted to DCHECK here that the Script in the existing entry
// matches the Script in the new key. However, replacing an existing Script
// can still happen in some edge cases that aren't common enough to be worth
// fixing. Consider the following unlikely sequence of events:
// 1. BackgroundMergeTask::SetUpOnMainThread finds a script S1 in the cache.
// 2. DevTools is attached and clears the cache.
// 3. DevTools is detached; the cache is reenabled.
// 4. A new instance of the script, S2, is compiled and placed into the cache.
// 5. The merge from step 1 finishes on the main thread, still using S1, and
// places S1 into the cache, replacing S2.
cache->SetKeyAt(entry, *k);
cache->SetPrimaryValueAt(entry, *value);
if (!found_existing) {
cache->ElementAdded();
}
return cache;
}
Handle<CompilationCacheTable> CompilationCacheTable::PutEval(
Handle<CompilationCacheTable> cache, Handle<String> src,
Handle<SharedFunctionInfo> outer_info, Handle<SharedFunctionInfo> value,
Handle<Context> native_context, Handle<FeedbackCell> feedback_cell,
int position) {
Isolate* isolate = native_context->GetIsolate();
src = String::Flatten(isolate, src);
EvalCacheKey key(src, outer_info, value->language_mode(), position);
// This block handles 'real' insertions, i.e. the initial dummy insert
// (below) has already happened earlier.
{
Handle<Object> k = key.AsHandle(isolate);
InternalIndex entry = cache->FindEntry(isolate, &key);
if (entry.is_found()) {
cache->SetKeyAt(entry, *k);
if (cache->PrimaryValueAt(entry) != *value) {
cache->SetPrimaryValueAt(entry, *value);
// The SFI is changing because the code was aged. Nuke existing feedback
// since it can't be reused after this point.
cache->SetEvalFeedbackValueAt(entry,
ReadOnlyRoots(isolate).the_hole_value());
}
// AddToFeedbackCellsMap may allocate a new sub-array to live in the
// entry, but it won't change the cache array. Therefore EntryToIndex
// and entry remains correct.
AddToFeedbackCellsMap(cache, entry, native_context, feedback_cell);
// Add hash again even on cache hit to avoid unnecessary cache delay in
// case of hash collisions.
}
}
// Create a dummy entry to mark that this key has already been inserted once.
cache = EnsureCapacity(isolate, cache);
InternalIndex entry = cache->FindInsertionEntry(isolate, key.Hash());
Handle<Object> k =
isolate->factory()->NewNumber(static_cast<double>(key.Hash()));
cache->SetKeyAt(entry, *k);
cache->SetPrimaryValueAt(entry, Smi::FromInt(kHashGenerations));
cache->ElementAdded();
return cache;
}
Handle<CompilationCacheTable> CompilationCacheTable::PutRegExp(
Isolate* isolate, Handle<CompilationCacheTable> cache, Handle<String> src,
JSRegExp::Flags flags, Handle<FixedArray> value) {
RegExpKey key(src, flags);
cache = EnsureCapacity(isolate, cache);
InternalIndex entry = cache->FindInsertionEntry(isolate, key.Hash());
// We store the value in the key slot, and compare the search key
// to the stored value with a custom IsMatch function during lookups.
cache->SetKeyAt(entry, *value);
cache->SetPrimaryValueAt(entry, *value);
cache->ElementAdded();
return cache;
}
void CompilationCacheTable::Remove(Tagged<Object> value) {
DisallowGarbageCollection no_gc;
for (InternalIndex entry : IterateEntries()) {
if (PrimaryValueAt(entry) == value) {
RemoveEntry(entry);
}
}
}
void CompilationCacheTable::RemoveEntry(InternalIndex entry) {
int entry_index = EntryToIndex(entry);
Tagged<Object> the_hole_value = GetReadOnlyRoots().the_hole_value();
for (int i = 0; i < kEntrySize; i++) {
this->set(entry_index + i, the_hole_value, SKIP_WRITE_BARRIER);
}
ElementRemoved();
// This table does not shrink upon deletion. The script cache depends on that
// fact, because EnsureScriptTableCapacity calls RemoveEntry at a time when
// shrinking the table would be counterproductive.
}
} // namespace internal
} // namespace v8