Background merging of deserialized scripts

Recently, https://crrev.com/c/v8/v8/+/3681880 added new API functions
with which an embedder could request that V8 merge newly deserialized
script data into an existing Script from the Isolate's compilation
cache. This change implements those new functions. This functionality is
still disabled by default due to the flag
merge_background_deserialized_script_with_compilation_cache.

The goal of this new functionality is to reduce memory usage when
multiple frames load the same script with a long delay between (long
enough for the script to have been evicted from Blink's in-memory cache
and for the top-level SharedFunctionInfo to be flushed). In that case,
there are two Script objects for the same script: one which was found in
the Isolate compilation cache (the "old" script), and one which was
recently deserialized (the "new" script). The new script's object graph
is essentially standalone: it may point to internalized strings and
readonly objects such as the empty feedback metadata, but otherwise
it is unconnected to the rest of the heap. The merging logic takes any
useful data from the new script's object graph and attaches it into the
old script's object graph, so that the new Script object and any other
duplicated objects can be discarded. More specifically:

1. If the new Script has a SharedFunctionInfo for a particular function
   literal, and the old Script does not, then the old Script is updated
   to refer to the new SharedFunctionInfo.
2. If the new Script has a compiled SharedFunctionInfo for a particular
   function literal, and the old Script has an uncompiled
   SharedFunctionInfo, then the old SharedFunctionInfo is updated to
   point to the function_data and feedback_metadata from the new
   SharedFunctionInfo.
3. If any used object from the new object graph points to a
   SharedFunctionInfo, where the old object graph contains a matching
   SharedFunctionInfo for the same function literal, then that pointer
   is updated to point to the old SharedFunctionInfo.

The document at [0] includes diagrams showing an example merge on a very
small script.

Steps 1 and 2 above are pretty simple, but step 3 requires walking a
possibly large set of objects, so this new API lets the embedder run
step 3 from a background thread. Steps 1 and 2 are performed later, on
the main thread.

The next important question is: in what ways can the old script's object
graph be modified during the background execution of step 3, or during
the time after step 3 but before steps 1 and 2?

A. SharedFunctionInfos can go from compiled to uncompiled due to
   flushing. This is okay; the worst outcome is that the function would
   need to be compiled again later. Such a risk is already present,
   since V8 doesn't keep IsCompiledScopes for every compiled function in
   a background-deserialized script.
B. SharedFunctionInfos can go from uncompiled to compiled due to lazy
   compilation. This is also okay; the merge completion logic on the
   main thread will just keep this lazily compiled data rather than
   inserting compiled data from the newly deserialized object graph.
C. SharedFunctionInfos can be cleared from the Script's weak array if
   they are no longer referenced. This is mostly okay, because any
   SharedFunctionInfo that is needed by the background merge is strongly
   referenced and therefore can't be cleared. The only problem arises if
   the top-level SharedFunctionInfo gets cleared, so the merge task must
   deliberately keep a reference to that one.
D. SharedFunctionInfos can be created if they are needed due to lazy
   compilation of a parent function. This change is somewhat troublesome
   because it invalidates the background thread's work and requires a
   re-traversal on the main thread to update any pointers that should
   point to this lazily compiled SharedFunctionInfo.

At a high level, this change implements three previously unimplemented
functions in BackgroundDeserializeTask (in compiler.cc) and updates one:

- BackgroundDeserializeTask::SourceTextAvailable, run on the main
  thread, checks whether there is a matching Script in the Isolate
  compilation cache which doesn't already have a top-level
  SharedFunctionInfo. If so, it saves that Script in a persistent
  handle.
- BackgroundDeserializeTask::ShouldMergeWithExistingScript checks
  whether the persistent handle from the first step exists (a fast
  operation which can be called from any thread).
- BackgroundDeserializeTask::MergeWithExistingScript, run on a
  background thread, performs step 3 of the merge described above and
  generates lists of persistent data describing how the main thread can
  complete the merge.
- BackgroundDeserializeTask::Finish is updated to perform the merge
  steps 1 and 2 listed above, as well as a possible re-traversal of the
  graph if required due to newly created SharedFunctionInfos in the old
  Script.

The merge logic has nothing to do with deserialization, and indeed I
hope to reuse it for background compilation tasks as well, so it is all
contained within a new class BackgroundMergeTask (in compiler.h,cc). It
uses a second class, ForwardPointersVisitor (in compiler.cc) to perform
the object visitation that updates pointers to SharedFunctionInfos.

[0] https://docs.google.com/document/d/1UksB5Vm7TT1-f3S9W1dK_rP9jKn_ly0WVm_UDPpWuBw/edit

Bug: v8:12808
Change-Id: Id405869e9d5b106ca7afd9c4b08cb5813e6852c6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3739232
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Seth Brenith <seth.brenith@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#81941}
This commit is contained in:
Seth Brenith 2022-07-25 08:39:46 -07:00 committed by V8 LUCI CQ
parent 4af624591b
commit e895b7af73
11 changed files with 919 additions and 32 deletions

View File

@ -1161,6 +1161,7 @@ filegroup(
"src/codegen/assembler.cc",
"src/codegen/assembler.h",
"src/codegen/atomic-memory-order.h",
"src/codegen/background-merge-task.h",
"src/codegen/bailout-reason.cc",
"src/codegen/bailout-reason.h",
"src/codegen/callable.h",

View File

@ -2791,6 +2791,7 @@ v8_header_set("v8_internal_headers") {
"src/codegen/assembler-inl.h",
"src/codegen/assembler.h",
"src/codegen/atomic-memory-order.h",
"src/codegen/background-merge-task.h",
"src/codegen/bailout-reason.h",
"src/codegen/callable.h",
"src/codegen/code-comments.h",

View File

@ -0,0 +1,87 @@
// Copyright 2022 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.
#ifndef V8_CODEGEN_BACKGROUND_MERGE_TASK_H_
#define V8_CODEGEN_BACKGROUND_MERGE_TASK_H_
#include <vector>
#include "src/handles/maybe-handles.h"
namespace v8 {
namespace internal {
class FeedbackMetadata;
class PersistentHandles;
class Script;
class SharedFunctionInfo;
class String;
struct ScriptDetails;
// Contains data transferred between threads for background merging between a
// newly compiled or deserialized script and an existing script from the Isolate
// compilation cache.
class V8_EXPORT_PRIVATE BackgroundMergeTask {
public:
// Step 1: on the main thread, check whether the Isolate compilation cache
// contains the script.
void SetUpOnMainThread(Isolate* isolate, Handle<String> source_text,
const ScriptDetails& script_details,
LanguageMode language_mode);
// Step 2: on the background thread, update pointers in the new Script's
// object graph to point to corresponding objects from the cached Script where
// appropriate. May only be called if HasCachedScript returned true.
void BeginMergeInBackground(LocalIsolate* isolate, Handle<Script> new_script);
// Step 3: on the main thread again, complete the merge so that all relevant
// objects are reachable from the cached Script. May only be called if
// HasPendingForegroundWork returned true. Returns the top-level
// SharedFunctionInfo that should be used.
Handle<SharedFunctionInfo> CompleteMergeInForeground(
Isolate* isolate, Handle<Script> new_script);
bool HasCachedScript() const { return !cached_script_.is_null(); }
bool HasPendingForegroundWork() const {
return !used_new_sfis_.empty() ||
!new_compiled_data_for_cached_sfis_.empty();
}
private:
std::unique_ptr<PersistentHandles> persistent_handles_;
// Data from main thread:
MaybeHandle<Script> cached_script_;
// Data from background thread:
// The top-level SharedFunctionInfo from the cached script, if one existed,
// just to keep it alive.
MaybeHandle<SharedFunctionInfo> toplevel_sfi_from_cached_script_;
// New SharedFunctionInfos which are used because there was no corresponding
// SharedFunctionInfo in the cached script. The main thread must:
// 1. Check whether the cached script gained corresponding SharedFunctionInfos
// for any of these, and if so, redo the merge.
// 2. Update the cached script's shared_function_infos list to refer to these.
std::vector<Handle<SharedFunctionInfo>> used_new_sfis_;
// SharedFunctionInfos from the cached script which were not compiled, with
// function_data and feedback_metadata from the corresponding new
// SharedFunctionInfo. If the SharedFunctionInfo from the cached script is
// still uncompiled when finishing, the main thread must set the two fields.
struct NewCompiledDataForCachedSfi {
Handle<SharedFunctionInfo> cached_sfi;
Handle<Object> function_data;
Handle<FeedbackMetadata> feedback_metadata;
};
std::vector<NewCompiledDataForCachedSfi> new_compiled_data_for_cached_sfis_;
};
} // namespace internal
} // namespace v8
#endif // V8_CODEGEN_BACKGROUND_MERGE_TASK_H_

View File

@ -52,6 +52,7 @@
#include "src/objects/js-function-inl.h"
#include "src/objects/map.h"
#include "src/objects/object-list-macros.h"
#include "src/objects/objects-body-descriptors-inl.h"
#include "src/objects/shared-function-info.h"
#include "src/objects/string.h"
#include "src/parsing/parse-info.h"
@ -1620,6 +1621,116 @@ void SetScriptFieldsFromDetails(Isolate* isolate, Script script,
}
}
#ifdef ENABLE_SLOW_DCHECKS
// A class which traverses the object graph for a newly compiled Script and
// ensures that it contains pointers to Scripts and SharedFunctionInfos only at
// the expected locations. Any failure in this visitor indicates a case that is
// probably not handled correctly in BackgroundMergeTask.
class MergeAssumptionChecker final : public ObjectVisitor {
public:
explicit MergeAssumptionChecker(PtrComprCageBase cage_base)
: cage_base_(cage_base) {}
void IterateObjects(HeapObject start) {
QueueVisit(start, kNormalObject);
while (to_visit_.size() > 0) {
std::pair<HeapObject, ObjectKind> pair = to_visit_.top();
to_visit_.pop();
HeapObject current = pair.first;
// The Script's shared_function_infos list and the constant pools for all
// BytecodeArrays are expected to contain pointers to SharedFunctionInfos.
// However, the type of those objects (FixedArray or WeakFixedArray)
// doesn't have enough information to indicate their usage, so we enqueue
// those objects here rather than during VisitPointers.
if (current.IsScript()) {
HeapObject sfis = Script::cast(current).shared_function_infos();
QueueVisit(sfis, kScriptSfiList);
} else if (current.IsBytecodeArray()) {
HeapObject constants = BytecodeArray::cast(current).constant_pool();
QueueVisit(constants, kConstantPool);
}
current_object_kind_ = pair.second;
current.IterateBody(cage_base_, this);
QueueVisit(current.map(), kNormalObject);
}
}
// ObjectVisitor implementation:
void VisitPointers(HeapObject host, ObjectSlot start,
ObjectSlot end) override {
MaybeObjectSlot maybe_start(start);
MaybeObjectSlot maybe_end(end);
VisitPointers(host, maybe_start, maybe_end);
}
void VisitPointers(HeapObject host, MaybeObjectSlot start,
MaybeObjectSlot end) override {
for (MaybeObjectSlot current = start; current != end; ++current) {
MaybeObject maybe_obj = current.load(cage_base_);
HeapObject obj;
bool is_weak = maybe_obj.IsWeak();
if (maybe_obj.GetHeapObject(&obj)) {
if (obj.IsSharedFunctionInfo()) {
CHECK((current_object_kind_ == kConstantPool && !is_weak) ||
(current_object_kind_ == kScriptSfiList && is_weak));
} else if (obj.IsScript()) {
CHECK(host.IsSharedFunctionInfo() &&
current == MaybeObjectSlot(
host.address() +
SharedFunctionInfo::kScriptOrDebugInfoOffset));
} else if (obj.IsFixedArray() &&
current_object_kind_ == kConstantPool) {
// Constant pools can contain nested fixed arrays, which in turn can
// point to SFIs.
QueueVisit(obj, kConstantPool);
}
QueueVisit(obj, kNormalObject);
}
}
}
// The object graph for a newly compiled Script shouldn't yet contain any
// Code. If any of these functions are called, then that would indicate that
// the graph was not disjoint from the rest of the heap as expected.
void VisitCodePointer(HeapObject host, CodeObjectSlot slot) override {
UNREACHABLE();
}
void VisitCodeTarget(Code host, RelocInfo* rinfo) override { UNREACHABLE(); }
void VisitEmbeddedPointer(Code host, RelocInfo* rinfo) override {
UNREACHABLE();
}
private:
enum ObjectKind {
kNormalObject,
kConstantPool,
kScriptSfiList,
};
// If the object hasn't yet been added to the worklist, add it. Subsequent
// calls with the same object have no effect, even if kind is different.
void QueueVisit(HeapObject obj, ObjectKind kind) {
if (visited_.insert(obj).second) {
to_visit_.push(std::make_pair(obj, kind));
}
}
DisallowGarbageCollection no_gc_;
PtrComprCageBase cage_base_;
std::stack<std::pair<HeapObject, ObjectKind>> to_visit_;
// Objects that are either in to_visit_ or done being visited. It is safe to
// use HeapObject directly here because GC is disallowed while running this
// visitor.
std::unordered_set<HeapObject, Object::Hasher> visited_;
ObjectKind current_object_kind_ = kNormalObject;
};
#endif // ENABLE_SLOW_DCHECKS
} // namespace
void BackgroundCompileTask::Run() {
@ -1742,6 +1853,11 @@ void BackgroundCompileTask::Run(
if (maybe_result.is_null()) {
PreparePendingException(isolate, &info);
} else if (FLAG_enable_slow_asserts) {
#ifdef ENABLE_SLOW_DCHECKS
MergeAssumptionChecker checker(isolate);
checker.IterateObjects(*maybe_result.ToHandleChecked());
#endif
}
outer_function_sfi_ = isolate->heap()->NewPersistentMaybeHandle(maybe_result);
@ -1749,6 +1865,228 @@ void BackgroundCompileTask::Run(
persistent_handles_ = isolate->heap()->DetachPersistentHandles();
}
// A class which traverses the constant pools of newly compiled
// SharedFunctionInfos and updates any pointers which need updating.
class ConstantPoolPointerForwarder {
public:
explicit ConstantPoolPointerForwarder(PtrComprCageBase cage_base,
LocalHeap* local_heap)
: cage_base_(cage_base), local_heap_(local_heap) {}
void AddBytecodeArray(BytecodeArray bytecode_array) {
bytecode_arrays_to_update_.push_back(handle(bytecode_array, local_heap_));
}
void Forward(SharedFunctionInfo from, SharedFunctionInfo to) {
forwarding_table_[from.function_literal_id()] = handle(to, local_heap_);
}
// Runs the update after the setup functions above specified the work to do.
void IterateAndForwardPointers() {
DCHECK(HasAnythingToForward());
for (Handle<BytecodeArray> bytecode_array : bytecode_arrays_to_update_) {
local_heap_->Safepoint();
DisallowGarbageCollection no_gc;
FixedArray constant_pool = bytecode_array->constant_pool();
IterateConstantPool(constant_pool);
}
}
bool HasAnythingToForward() const { return !forwarding_table_.empty(); }
private:
void IterateConstantPool(FixedArray constant_pool) {
for (int i = 0, length = constant_pool.length(); i < length; ++i) {
Object obj = constant_pool.get(i);
if (obj.IsSmi()) continue;
HeapObject heap_obj = HeapObject::cast(obj);
if (heap_obj.IsFixedArray(cage_base_)) {
// Constant pools can have nested fixed arrays, but such relationships
// are acyclic and never more than a few layers deep, so recursion is
// fine here.
IterateConstantPool(FixedArray::cast(heap_obj));
} else if (heap_obj.IsSharedFunctionInfo(cage_base_)) {
auto it = forwarding_table_.find(
SharedFunctionInfo::cast(heap_obj).function_literal_id());
if (it != forwarding_table_.end()) {
constant_pool.set(i, *it->second);
}
}
}
}
PtrComprCageBase cage_base_;
LocalHeap* local_heap_;
std::vector<Handle<BytecodeArray>> bytecode_arrays_to_update_;
// If any SharedFunctionInfo is found in constant pools with a function
// literal ID matching one of these keys, then that entry should be updated
// to point to the corresponding value.
std::unordered_map<int, Handle<SharedFunctionInfo>> forwarding_table_;
};
void BackgroundMergeTask::SetUpOnMainThread(Isolate* isolate,
Handle<String> source_text,
const ScriptDetails& script_details,
LanguageMode language_mode) {
HandleScope handle_scope(isolate);
CompilationCacheScript::LookupResult lookup_result =
isolate->compilation_cache()->LookupScript(source_text, script_details,
language_mode);
Handle<Script> script;
if (!lookup_result.script().ToHandle(&script)) return;
// Any data sent to the background thread will need to be a persistent handle.
persistent_handles_ = std::make_unique<PersistentHandles>(isolate);
if (lookup_result.is_compiled_scope().is_compiled()) {
// There already exists a compiled top-level SFI, so the main thread will
// discard the background serialization results and use the top-level SFI
// from the cache, assuming the top-level SFI is still compiled by then.
// Thus, there is no need to keep the Script pointer for background merging.
// Do nothing in this case.
} else {
DCHECK(lookup_result.toplevel_sfi().is_null());
// A background merge is required.
cached_script_ = persistent_handles_->NewHandle(*script);
}
}
void BackgroundMergeTask::BeginMergeInBackground(LocalIsolate* isolate,
Handle<Script> new_script) {
LocalHeap* local_heap = isolate->heap();
local_heap->AttachPersistentHandles(std::move(persistent_handles_));
LocalHandleScope handle_scope(local_heap);
ConstantPoolPointerForwarder forwarder(isolate, local_heap);
Handle<Script> old_script = cached_script_.ToHandleChecked();
{
DisallowGarbageCollection no_gc;
MaybeObject maybe_old_toplevel_sfi =
old_script->shared_function_infos().Get(kFunctionLiteralIdTopLevel);
if (maybe_old_toplevel_sfi.IsWeak()) {
SharedFunctionInfo old_toplevel_sfi = SharedFunctionInfo::cast(
maybe_old_toplevel_sfi.GetHeapObjectAssumeWeak());
toplevel_sfi_from_cached_script_ =
local_heap->NewPersistentHandle(old_toplevel_sfi);
}
}
// Iterate the SFI lists on both Scripts to set up the forwarding table and
// follow-up worklists for the main thread.
CHECK_EQ(old_script->shared_function_infos().length(),
new_script->shared_function_infos().length());
for (int i = 0; i < old_script->shared_function_infos().length(); ++i) {
DisallowGarbageCollection no_gc;
MaybeObject maybe_new_sfi = new_script->shared_function_infos().Get(i);
if (maybe_new_sfi.IsWeak()) {
SharedFunctionInfo new_sfi =
SharedFunctionInfo::cast(maybe_new_sfi.GetHeapObjectAssumeWeak());
MaybeObject maybe_old_sfi = old_script->shared_function_infos().Get(i);
if (maybe_old_sfi.IsWeak()) {
// The old script and the new script both have SharedFunctionInfos for
// this function literal.
SharedFunctionInfo old_sfi =
SharedFunctionInfo::cast(maybe_old_sfi.GetHeapObjectAssumeWeak());
forwarder.Forward(new_sfi, old_sfi);
if (new_sfi.is_compiled()) {
if (old_sfi.is_compiled()) {
// Reset the old SFI's bytecode age so that it won't likely get
// flushed right away. This operation might be racing against
// concurrent modification by another thread, but such a race is not
// catastrophic.
old_sfi.GetBytecodeArray(isolate).set_bytecode_age(0);
} else {
// The old SFI can use the compiled data from the new SFI.
Object function_data = new_sfi.function_data(kAcquireLoad);
FeedbackMetadata feedback_metadata = new_sfi.feedback_metadata();
new_compiled_data_for_cached_sfis_.push_back(
{local_heap->NewPersistentHandle(old_sfi),
local_heap->NewPersistentHandle(function_data),
local_heap->NewPersistentHandle(feedback_metadata)});
forwarder.AddBytecodeArray(new_sfi.GetBytecodeArray(isolate));
}
}
} else {
// The old script didn't have a SharedFunctionInfo for this function
// literal, so it can use the new SharedFunctionInfo.
DCHECK_EQ(i, new_sfi.function_literal_id());
new_sfi.set_script(*old_script);
used_new_sfis_.push_back(local_heap->NewPersistentHandle(new_sfi));
if (new_sfi.is_compiled()) {
forwarder.AddBytecodeArray(new_sfi.GetBytecodeArray(isolate));
}
}
}
}
persistent_handles_ = local_heap->DetachPersistentHandles();
if (forwarder.HasAnythingToForward()) {
forwarder.IterateAndForwardPointers();
}
}
Handle<SharedFunctionInfo> BackgroundMergeTask::CompleteMergeInForeground(
Isolate* isolate, Handle<Script> new_script) {
HandleScope handle_scope(isolate);
ConstantPoolPointerForwarder forwarder(isolate,
isolate->main_thread_local_heap());
Handle<Script> old_script = cached_script_.ToHandleChecked();
for (const auto& new_compiled_data : new_compiled_data_for_cached_sfis_) {
if (!new_compiled_data.cached_sfi->is_compiled()) {
new_compiled_data.cached_sfi->set_function_data(
*new_compiled_data.function_data, kReleaseStore);
new_compiled_data.cached_sfi->set_feedback_metadata(
*new_compiled_data.feedback_metadata, kReleaseStore);
}
}
for (Handle<SharedFunctionInfo> new_sfi : used_new_sfis_) {
DisallowGarbageCollection no_gc;
DCHECK_GE(new_sfi->function_literal_id(), 0);
MaybeObject maybe_old_sfi =
old_script->shared_function_infos().Get(new_sfi->function_literal_id());
if (maybe_old_sfi.IsWeak()) {
// The old script's SFI didn't exist during the background work, but
// does now. This means a re-merge is necessary so that any pointers to
// the new script's SFI are updated to point to the old script's SFI.
SharedFunctionInfo old_sfi =
SharedFunctionInfo::cast(maybe_old_sfi.GetHeapObjectAssumeWeak());
forwarder.Forward(*new_sfi, old_sfi);
} else {
old_script->shared_function_infos().Set(
new_sfi->function_literal_id(),
MaybeObject::MakeWeak(MaybeObject::FromObject(*new_sfi)));
}
}
// Most of the time, the background merge was sufficient. However, if there
// are any new pointers that need forwarding, a new traversal of the constant
// pools is required.
if (forwarder.HasAnythingToForward()) {
for (Handle<SharedFunctionInfo> new_sfi : used_new_sfis_) {
forwarder.AddBytecodeArray(new_sfi->GetBytecodeArray(isolate));
}
for (const auto& new_compiled_data : new_compiled_data_for_cached_sfis_) {
forwarder.AddBytecodeArray(
new_compiled_data.cached_sfi->GetBytecodeArray(isolate));
}
forwarder.IterateAndForwardPointers();
}
MaybeObject maybe_toplevel_sfi =
old_script->shared_function_infos().Get(kFunctionLiteralIdTopLevel);
CHECK(maybe_toplevel_sfi.IsWeak());
Handle<SharedFunctionInfo> result = handle(
SharedFunctionInfo::cast(maybe_toplevel_sfi.GetHeapObjectAssumeWeak()),
isolate);
return handle_scope.CloseAndEscape(result);
}
MaybeHandle<SharedFunctionInfo> BackgroundCompileTask::FinalizeScript(
Isolate* isolate, Handle<String> source,
const ScriptDetails& script_details) {
@ -1878,24 +2216,38 @@ void BackgroundDeserializeTask::Run() {
Handle<SharedFunctionInfo> inner_result;
off_thread_data_ =
CodeSerializer::StartDeserializeOffThread(&isolate, &cached_data_);
if (FLAG_enable_slow_asserts && off_thread_data_.HasResult()) {
#ifdef ENABLE_SLOW_DCHECKS
MergeAssumptionChecker checker(&isolate);
checker.IterateObjects(*off_thread_data_.GetOnlyScript(isolate.heap()));
#endif
}
}
void BackgroundDeserializeTask::SourceTextAvailable(
Isolate* isolate, Handle<String> source_text,
const ScriptDetails& script_details) {
DCHECK_EQ(isolate, isolate_for_local_isolate_);
// TODO(v8:12808): Implement this.
LanguageMode language_mode = construct_language_mode(FLAG_use_strict);
background_merge_task_.SetUpOnMainThread(isolate, source_text, script_details,
language_mode);
}
bool BackgroundDeserializeTask::ShouldMergeWithExistingScript() const {
DCHECK(FLAG_merge_background_deserialized_script_with_compilation_cache);
// TODO(v8:12808): Implement this.
return true;
return background_merge_task_.HasCachedScript() &&
off_thread_data_.HasResult();
}
void BackgroundDeserializeTask::MergeWithExistingScript() {
DCHECK(FLAG_merge_background_deserialized_script_with_compilation_cache);
// TODO(v8:12808): Implement this.
LocalIsolate isolate(isolate_for_local_isolate_, ThreadKind::kBackground);
UnparkedScope unparked_scope(&isolate);
LocalHandleScope handle_scope(isolate.heap());
background_merge_task_.BeginMergeInBackground(
&isolate, off_thread_data_.GetOnlyScript(isolate.heap()));
}
MaybeHandle<SharedFunctionInfo> BackgroundDeserializeTask::Finish(
@ -1903,7 +2255,7 @@ MaybeHandle<SharedFunctionInfo> BackgroundDeserializeTask::Finish(
ScriptOriginOptions origin_options) {
return CodeSerializer::FinishOffThreadDeserialize(
isolate, std::move(off_thread_data_), &cached_data_, source,
origin_options);
origin_options, &background_merge_task_);
}
// ----------------------------------------------------------------------------
@ -3073,17 +3425,33 @@ 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,
script_details.origin_options);
// It is possible at this point that there is a Script object for this
// script in the compilation cache (held in the variable maybe_script),
// which does not match maybe_result->script(). This could happen any of
// three ways:
// 1. The embedder didn't call MergeWithExistingScript.
// 2. At the time the embedder called SourceTextAvailable, there was not
// yet a Script in the compilation cache, but it arrived sometime
// later.
// 3. At the time the embedder called SourceTextAvailable, there was a
// Script available, and the new content has been merged into that
// Script. However, since then, the Script was replaced in the
// compilation cache, such as by another evaluation of the script
// hitting case 2, or DevTools clearing the cache.
// This is okay; the new Script object will replace the current Script
// held by the compilation cache. Both Scripts may remain in use
// indefinitely, causing increased memory usage, but these cases are
// sufficiently unlikely, and ensuring a correct merge in the third case
// would be non-trivial.
} else {
maybe_result = CodeSerializer::Deserialize(
isolate, cached_data, source, script_details.origin_options);
// TODO(v8:12808): Merge the newly deserialized code into a preexisting
// Script if one was found in the compilation cache.
}
bool consuming_code_cache_succeeded = false;

View File

@ -11,6 +11,7 @@
#include "src/ast/ast-value-factory.h"
#include "src/base/platform/elapsed-timer.h"
#include "src/base/small-vector.h"
#include "src/codegen/background-merge-task.h"
#include "src/codegen/bailout-reason.h"
#include "src/common/globals.h"
#include "src/execution/isolate.h"
@ -635,6 +636,7 @@ class V8_EXPORT_PRIVATE BackgroundDeserializeTask {
Isolate* isolate_for_local_isolate_;
AlignedCachedData cached_data_;
CodeSerializer::OffThreadDeserializeData off_thread_data_;
BackgroundMergeTask background_merge_task_;
};
} // namespace internal

View File

@ -485,9 +485,16 @@ Handle<CompilationCacheTable> CompilationCacheTable::PutScript(
cache = EnsureScriptTableCapacity(isolate, cache);
entry = cache->FindInsertionEntry(isolate, key.Hash());
}
// TODO(v8:12808): Once all code paths are updated to reuse a Script if
// available, we could DCHECK here that the Script in the existing entry
// matches the Script in the new key. For now, there is no such guarantee.
// 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) {

View File

@ -422,6 +422,9 @@ class WeakArrayList
// duplicates.
V8_EXPORT_PRIVATE bool RemoveOne(const MaybeObjectHandle& value);
// Searches the array (linear time) and returns whether it contains the value.
V8_EXPORT_PRIVATE bool Contains(MaybeObject value);
class Iterator;
private:

View File

@ -4285,6 +4285,13 @@ bool WeakArrayList::RemoveOne(const MaybeObjectHandle& value) {
return false;
}
bool WeakArrayList::Contains(MaybeObject value) {
for (int i = 0; i < length(); ++i) {
if (Get(i) == value) return true;
}
return false;
}
// static
Handle<WeakArrayList> PrototypeUsers::Add(Isolate* isolate,
Handle<WeakArrayList> array,

View File

@ -9,6 +9,7 @@
#include "src/base/logging.h"
#include "src/base/platform/elapsed-timer.h"
#include "src/base/platform/platform.h"
#include "src/codegen/background-merge-task.h"
#include "src/common/globals.h"
#include "src/handles/maybe-handles.h"
#include "src/handles/persistent-handles.h"
@ -465,6 +466,25 @@ MaybeHandle<SharedFunctionInfo> CodeSerializer::Deserialize(
return scope.CloseAndEscape(result);
}
Handle<Script> CodeSerializer::OffThreadDeserializeData::GetOnlyScript(
LocalHeap* heap) {
std::unique_ptr<PersistentHandles> previous_persistent_handles =
heap->DetachPersistentHandles();
heap->AttachPersistentHandles(std::move(persistent_handles));
DCHECK_EQ(scripts.size(), 1);
// Make a non-persistent handle to return.
Handle<Script> script = handle(*scripts[0], heap);
DCHECK_EQ(*script, maybe_result.ToHandleChecked()->script());
persistent_handles = heap->DetachPersistentHandles();
if (previous_persistent_handles) {
heap->AttachPersistentHandles(std::move(previous_persistent_handles));
}
return script;
}
CodeSerializer::OffThreadDeserializeData
CodeSerializer::StartDeserializeOffThread(LocalIsolate* local_isolate,
AlignedCachedData* cached_data) {
@ -496,7 +516,8 @@ CodeSerializer::StartDeserializeOffThread(LocalIsolate* local_isolate,
MaybeHandle<SharedFunctionInfo> CodeSerializer::FinishOffThreadDeserialize(
Isolate* isolate, OffThreadDeserializeData&& data,
AlignedCachedData* cached_data, Handle<String> source,
ScriptOriginOptions origin_options) {
ScriptOriginOptions origin_options,
BackgroundMergeTask* background_merge_task) {
base::ElapsedTimer timer;
if (FLAG_profile_deserialization || FLAG_log_function_events) timer.Start();
@ -543,23 +564,32 @@ MaybeHandle<SharedFunctionInfo> CodeSerializer::FinishOffThreadDeserialize(
DCHECK(data.persistent_handles->Contains(result.location()));
result = handle(*result, isolate);
// Fix up the source on the script. This should be the only deserialized
// script, and the off-thread deserializer should have set its source to
// the empty string.
DCHECK_EQ(data.scripts.size(), 1);
DCHECK_EQ(result->script(), *data.scripts[0]);
DCHECK_EQ(Script::cast(result->script()).source(),
ReadOnlyRoots(isolate).empty_string());
Script::cast(result->script()).set_source(*source);
if (background_merge_task &&
background_merge_task->HasPendingForegroundWork()) {
Handle<Script> script = handle(Script::cast(result->script()), isolate);
result = background_merge_task->CompleteMergeInForeground(isolate, script);
DCHECK(Script::cast(result->script()).source().StrictEquals(*source));
DCHECK(isolate->factory()->script_list()->Contains(
MaybeObject::MakeWeak(MaybeObject::FromObject(result->script()))));
} else {
// Fix up the source on the script. This should be the only deserialized
// script, and the off-thread deserializer should have set its source to
// the empty string.
DCHECK_EQ(data.scripts.size(), 1);
DCHECK_EQ(result->script(), *data.scripts[0]);
DCHECK_EQ(Script::cast(result->script()).source(),
ReadOnlyRoots(isolate).empty_string());
Script::cast(result->script()).set_source(*source);
// Fix up the script list to include the newly deserialized script.
Handle<WeakArrayList> list = isolate->factory()->script_list();
for (Handle<Script> script : data.scripts) {
DCHECK(data.persistent_handles->Contains(script.location()));
list =
WeakArrayList::AddToEnd(isolate, list, MaybeObjectHandle::Weak(script));
// Fix up the script list to include the newly deserialized script.
Handle<WeakArrayList> list = isolate->factory()->script_list();
for (Handle<Script> script : data.scripts) {
DCHECK(data.persistent_handles->Contains(script.location()));
list = WeakArrayList::AddToEnd(isolate, list,
MaybeObjectHandle::Weak(script));
}
isolate->heap()->SetRootScriptList(*list);
}
isolate->heap()->SetRootScriptList(*list);
if (FLAG_profile_deserialization) {
double ms = timer.Elapsed().InMillisecondsF();

View File

@ -13,6 +13,7 @@ namespace v8 {
namespace internal {
class PersistentHandles;
class BackgroundMergeTask;
class V8_EXPORT_PRIVATE AlignedCachedData {
public:
@ -62,6 +63,10 @@ enum class SerializedCodeSanityCheckResult {
class CodeSerializer : public Serializer {
public:
struct OffThreadDeserializeData {
public:
bool HasResult() const { return !maybe_result.is_null(); }
Handle<Script> GetOnlyScript(LocalHeap* heap);
private:
friend class CodeSerializer;
MaybeHandle<SharedFunctionInfo> maybe_result;
@ -87,10 +92,11 @@ class CodeSerializer : public Serializer {
AlignedCachedData* cached_data);
V8_WARN_UNUSED_RESULT static MaybeHandle<SharedFunctionInfo>
FinishOffThreadDeserialize(Isolate* isolate, OffThreadDeserializeData&& data,
AlignedCachedData* cached_data,
Handle<String> source,
ScriptOriginOptions origin_options);
FinishOffThreadDeserialize(
Isolate* isolate, OffThreadDeserializeData&& data,
AlignedCachedData* cached_data, Handle<String> source,
ScriptOriginOptions origin_options,
BackgroundMergeTask* background_merge_task = nullptr);
uint32_t source_hash() const { return source_hash_; }

View File

@ -237,4 +237,379 @@ TEST_F(DeserializeTest, OffThreadDeserializeRejectsDifferentSource) {
}
}
class MergeDeserializedCodeTest : public DeserializeTest {
protected:
// The source code used in these tests.
static constexpr char kSourceCode[] = R"(
// Looks like an IIFE but isn't, to get eagerly parsed:
var eager = (function () {
// Actual IIFE, also eagerly parsed:
return (function iife() {
return 42;
})();
});
// Lazily parsed:
var lazy = function () { return eager(); };
)";
// Objects from the Script's object graph whose lifetimes and connectedness
// are useful to track.
enum ScriptObject {
kScript,
kToplevelSfi,
kToplevelFunctionData,
kToplevelFeedbackMetadata,
kEagerSfi,
kEagerFunctionData,
kEagerFeedbackMetadata,
kIifeSfi,
kIifeFunctionData,
kIifeFeedbackMetadata,
kLazySfi,
kScriptObjectsCount
};
enum ScriptObjectFlag {
kNone,
kScriptFlag = 1 << kScript,
kToplevelSfiFlag = 1 << kToplevelSfi,
kToplevelFunctionDataFlag = 1 << kToplevelFunctionData,
kToplevelFeedbackMetadataFlag = 1 << kToplevelFeedbackMetadata,
kEagerSfiFlag = 1 << kEagerSfi,
kEagerFunctionDataFlag = 1 << kEagerFunctionData,
kEagerFeedbackMetadataFlag = 1 << kEagerFeedbackMetadata,
kIifeSfiFlag = 1 << kIifeSfi,
kIifeFunctionDataFlag = 1 << kIifeFunctionData,
kIifeFeedbackMetadataFlag = 1 << kIifeFeedbackMetadata,
kLazySfiFlag = 1 << kLazySfi,
kAllScriptObjects = (1 << kScriptObjectsCount) - 1,
kAllCompiledSfis = kToplevelSfiFlag | kEagerSfiFlag | kIifeSfiFlag,
kAllSfis = kAllCompiledSfis | kLazySfiFlag,
kEagerAndLazy = kLazySfiFlag | kEagerSfiFlag,
kToplevelEagerAndLazy = kToplevelSfiFlag | kEagerAndLazy,
kToplevelAndEager = kToplevelSfiFlag | kEagerSfiFlag,
};
template <typename T>
static i::SharedFunctionInfo GetSharedFunctionInfo(
Local<T> function_or_script) {
i::Handle<i::JSFunction> i_function =
i::Handle<i::JSFunction>::cast(Utils::OpenHandle(*function_or_script));
return i_function->shared();
}
static i::MaybeObject WeakOrSmi(i::Object obj) {
return obj.IsSmi()
? i::MaybeObject::FromSmi(i::Smi::cast(obj))
: i::MaybeObject::MakeWeak(i::MaybeObject::FromObject(obj));
}
void ValidateStandaloneGraphAndPopulateArray(
i::SharedFunctionInfo toplevel_sfi, i::WeakFixedArray array,
bool lazy_should_be_compiled = false,
bool eager_should_be_compiled = true) {
i::DisallowGarbageCollection no_gc;
CHECK(toplevel_sfi.is_compiled());
array.Set(kToplevelSfi, WeakOrSmi(toplevel_sfi));
array.Set(kToplevelFunctionData,
WeakOrSmi(toplevel_sfi.function_data(kAcquireLoad)));
array.Set(kToplevelFeedbackMetadata,
WeakOrSmi(toplevel_sfi.feedback_metadata()));
i::Script script = i::Script::cast(toplevel_sfi.script());
array.Set(kScript, WeakOrSmi(script));
i::WeakFixedArray sfis = script.shared_function_infos();
CHECK_EQ(sfis.length(), 4);
CHECK_EQ(sfis.Get(0), WeakOrSmi(toplevel_sfi));
i::SharedFunctionInfo eager =
i::SharedFunctionInfo::cast(sfis.Get(1).GetHeapObjectAssumeWeak());
CHECK_EQ(eager.is_compiled(), eager_should_be_compiled);
array.Set(kEagerSfi, WeakOrSmi(eager));
if (eager_should_be_compiled) {
array.Set(kEagerFunctionData,
WeakOrSmi(eager.function_data(kAcquireLoad)));
array.Set(kEagerFeedbackMetadata, WeakOrSmi(eager.feedback_metadata()));
i::SharedFunctionInfo iife =
i::SharedFunctionInfo::cast(sfis.Get(2).GetHeapObjectAssumeWeak());
CHECK(iife.is_compiled());
array.Set(kIifeSfi, WeakOrSmi(iife));
array.Set(kIifeFunctionData, WeakOrSmi(iife.function_data(kAcquireLoad)));
array.Set(kIifeFeedbackMetadata, WeakOrSmi(iife.feedback_metadata()));
}
i::SharedFunctionInfo lazy =
i::SharedFunctionInfo::cast(sfis.Get(3).GetHeapObjectAssumeWeak());
CHECK_EQ(lazy.is_compiled(), lazy_should_be_compiled);
array.Set(kLazySfi, WeakOrSmi(lazy));
}
void AgeBytecodeAndGC(ScriptObjectFlag sfis_to_age,
i::Handle<i::WeakFixedArray> original_objects,
i::Isolate* i_isolate) {
for (int index = 0; index < kScriptObjectsCount; ++index) {
if ((sfis_to_age & (1 << index)) == (1 << index)) {
i::BytecodeArray bytecode =
i::SharedFunctionInfo::cast(
original_objects->Get(index).GetHeapObjectAssumeWeak())
.GetBytecodeArray(i_isolate);
const int kAgingThreshold = 6;
for (int j = 0; j < kAgingThreshold; ++j) {
bytecode.MakeOlder();
}
}
}
i_isolate->heap()->CollectAllGarbage(i::Heap::kNoGCFlags,
i::GarbageCollectionReason::kTesting);
}
class MergeThread : public base::Thread {
public:
explicit MergeThread(ScriptCompiler::ConsumeCodeCacheTask* task)
: Thread(base::Thread::Options("MergeThread")), task_(task) {}
void Run() override { task_->MergeWithExistingScript(); }
private:
ScriptCompiler::ConsumeCodeCacheTask* task_;
};
void RetainObjects(ScriptObjectFlag to_retain,
i::WeakFixedArray original_objects,
i::FixedArray retained_original_objects,
i::Isolate* i_isolate) {
for (int index = 0; index < kScriptObjectsCount; ++index) {
if ((to_retain & (1 << index)) == (1 << index)) {
i::MaybeObject maybe = original_objects.Get(index);
if (i::HeapObject heap_object;
maybe.GetHeapObjectIfWeak(&heap_object)) {
retained_original_objects.set(index, heap_object);
continue;
}
}
retained_original_objects.set(
index, i::ReadOnlyRoots(i_isolate).undefined_value());
}
}
void TestOffThreadMerge(ScriptObjectFlag retained_before_background_merge,
ScriptObjectFlag aged_before_background_merge,
bool run_code_after_background_merge,
ScriptObjectFlag retained_after_background_merge,
ScriptObjectFlag aged_after_background_merge,
bool lazy_should_be_compiled = false,
bool eager_should_be_compiled = true) {
i::FLAG_merge_background_deserialized_script_with_compilation_cache = true;
std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data;
IsolateAndContextScope scope(this);
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate());
ScriptOrigin default_origin(isolate(), NewString(""));
i::Handle<i::WeakFixedArray> original_objects =
i_isolate->factory()->NewWeakFixedArray(kScriptObjectsCount);
i::Handle<i::FixedArray> retained_original_objects =
i_isolate->factory()->NewFixedArray(kScriptObjectsCount);
i::Handle<i::WeakFixedArray> new_objects =
i_isolate->factory()->NewWeakFixedArray(kScriptObjectsCount);
Local<Script> original_script;
// Compile the script for the first time, to both populate the Isolate
// compilation cache and produce code cache data.
{
v8::EscapableHandleScope handle_scope(isolate());
Local<Script> script =
Script::Compile(context(), NewString(kSourceCode), &default_origin)
.ToLocalChecked();
ValidateStandaloneGraphAndPopulateArray(GetSharedFunctionInfo(script),
*original_objects);
RetainObjects(retained_before_background_merge, *original_objects,
*retained_original_objects, i_isolate);
cached_data.reset(
ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
if (run_code_after_background_merge) {
// We must retain the v8::Script (a JSFunction) so we can run it later.
original_script = handle_scope.Escape(script);
// It doesn't make any sense to configure a test case which says it
// doesn't want to retain the toplevel SFI but does want to run the
// script later.
CHECK(retained_before_background_merge & kToplevelSfiFlag);
}
}
AgeBytecodeAndGC(aged_before_background_merge, original_objects, i_isolate);
DeserializeThread deserialize_thread(
ScriptCompiler::StartConsumingCodeCache(
isolate(), std::make_unique<ScriptCompiler::CachedData>(
cached_data->data, cached_data->length,
ScriptCompiler::CachedData::BufferNotOwned)));
CHECK(deserialize_thread.Start());
deserialize_thread.Join();
std::unique_ptr<ScriptCompiler::ConsumeCodeCacheTask> task =
deserialize_thread.TakeTask();
task->SourceTextAvailable(isolate(), NewString(kSourceCode),
default_origin);
// If the top-level SFI was retained and not flushed, then no merge is
// necessary because the results from the deserialization will be discarded.
// If nothing at all was retained, then no merge is necessary because the
// original Script is no longer in the compilation cache. Otherwise, a merge
// is necessary.
bool merge_expected =
(retained_before_background_merge != kNone) &&
(!(retained_before_background_merge & kToplevelSfiFlag) ||
(aged_before_background_merge & kToplevelSfiFlag));
CHECK_EQ(merge_expected, task->ShouldMergeWithExistingScript());
if (merge_expected) {
MergeThread merge_thread(task.get());
CHECK(merge_thread.Start());
merge_thread.Join();
}
if (run_code_after_background_merge) {
CHECK(!original_script->Run(context()).IsEmpty());
CHECK_EQ(RunGlobalFunc("lazy"), v8::Integer::New(isolate(), 42));
ValidateStandaloneGraphAndPopulateArray(
GetSharedFunctionInfo(original_script), *original_objects,
true /*lazy_should_be_compiled*/);
}
RetainObjects(retained_after_background_merge, *original_objects,
*retained_original_objects, i_isolate);
AgeBytecodeAndGC(aged_after_background_merge, original_objects, i_isolate);
ScriptCompiler::Source source(NewString(kSourceCode), default_origin,
cached_data.release(), task.release());
Local<Script> script =
ScriptCompiler::Compile(context(), &source,
ScriptCompiler::kConsumeCodeCache)
.ToLocalChecked();
CHECK(!source.GetCachedData()->rejected);
ValidateStandaloneGraphAndPopulateArray(
GetSharedFunctionInfo(script), *new_objects, lazy_should_be_compiled,
eager_should_be_compiled);
// At this point, the original_objects array might still have pointers to
// some old discarded content, such as UncompiledData from flushed
// functions. GC again to clear it all out.
i_isolate->heap()->CollectAllGarbage(i::Heap::kNoGCFlags,
i::GarbageCollectionReason::kTesting);
// All tracked objects from the original Script should have been reused if
// they're still alive.
for (int index = 0; index < kScriptObjectsCount; ++index) {
if (original_objects->Get(index).IsWeak() &&
new_objects->Get(index).IsWeak()) {
CHECK_EQ(original_objects->Get(index), new_objects->Get(index));
}
}
CHECK(!script->Run(context()).IsEmpty());
CHECK_EQ(RunGlobalFunc("lazy"), v8::Integer::New(isolate(), 42));
}
};
TEST_F(MergeDeserializedCodeTest, NoMergeWhenAlreadyCompiled) {
// Retain everything; age nothing.
TestOffThreadMerge(kAllScriptObjects, // retained_before_background_merge
kNone, // aged_before_background_merge
false, // run_code_after_background_merge
kAllScriptObjects, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, NoMergeWhenOriginalWasDiscarded) {
// Retain nothing.
TestOffThreadMerge(kNone, // retained_before_background_merge
kNone, // aged_before_background_merge
false, // run_code_after_background_merge
kNone, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, NoMergeWhenOriginalWasDiscardedLate) {
// The original top-level SFI is retained by the background merge task even
// though other retainers are discarded.
TestOffThreadMerge(kAllScriptObjects, // retained_before_background_merge
kNone, // aged_before_background_merge
false, // run_code_after_background_merge
kNone, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, MergeIntoFlushedSFIs) {
// Retain all SFIs but age them.
TestOffThreadMerge(kAllSfis, // retained_before_background_merge
kAllCompiledSfis, // aged_before_background_merge
false, // run_code_after_background_merge
kAllSfis, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, MergeBasic) {
// Retain the eager and lazy functions; discard the top-level SFI.
// This is a common scenario which requires a merge.
TestOffThreadMerge(kEagerAndLazy, // retained_before_background_merge
kToplevelSfiFlag, // aged_before_background_merge
false, // run_code_after_background_merge
kNone, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, MergeBasicWithFlushing) {
// Retain the eager and lazy functions; discard the top-level SFI.
// Also flush the eager function, which discards the IIFE.
// This is a common scenario which requires a merge.
TestOffThreadMerge(kEagerAndLazy, // retained_before_background_merge
kToplevelAndEager, // aged_before_background_merge
false, // run_code_after_background_merge
kNone, // retained_after_background_merge
kNone); // aged_after_background_merge
}
TEST_F(MergeDeserializedCodeTest, MergeBasicWithLateFlushing) {
// Flush the eager function after the background merge has taken place. In
// this case, the data from the background thread points to the eager SFI but
// not its bytecode, so the end result is that the eager SFI is not compiled
// after completion on the main thread.
TestOffThreadMerge(kEagerAndLazy, // retained_before_background_merge
kToplevelSfiFlag, // aged_before_background_merge
false, // run_code_after_background_merge
kNone, // retained_after_background_merge
kEagerSfiFlag, // aged_after_background_merge
false, // lazy_should_be_compiled
false); // eager_should_be_compiled
}
TEST_F(MergeDeserializedCodeTest, RunScriptButNoReMergeNecessary) {
// The original script is run after the background merge, causing the
// top-level SFI and lazy SFI to become compiled. However, no SFIs are
// created when running the script, so the main thread needn't redo the merge.
TestOffThreadMerge(kToplevelEagerAndLazy, // retained_before_background_merge
kToplevelSfiFlag, // aged_before_background_merge
true, // run_code_after_background_merge
kAllScriptObjects, // retained_after_background_merge
kNone, // aged_after_background_merge
true); // lazy_should_be_compiled
}
TEST_F(MergeDeserializedCodeTest, MainThreadReMerge) {
// By flushing the eager SFI early, we cause the IIFE SFI to disappear
// entirely. When the original script runs after the background merge, the
// IIFE SFI is recreated. Thus, the main thread must redo the merge.
TestOffThreadMerge(kToplevelEagerAndLazy, // retained_before_background_merge
kToplevelAndEager, // aged_before_background_merge
true, // run_code_after_background_merge
kAllScriptObjects, // retained_after_background_merge
kToplevelSfiFlag, // aged_after_background_merge
true); // lazy_should_be_compiled
}
} // namespace v8