Reland "Background merging of deserialized scripts"
This is a reland of commit e895b7af73
The unit test has been updated to work correctly when
--stress-incremental-marking is enabled.
Original change's description:
> 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}
Bug: v8:12808
Change-Id: Id2036dfa4eba8670cac899773d7a906825fa2c50
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3787266
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Seth Brenith <seth.brenith@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#82045}
This commit is contained in:
parent
c34c85a53b
commit
766b2a4d52
@ -1160,6 +1160,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",
|
||||
|
1
BUILD.gn
1
BUILD.gn
@ -2792,6 +2792,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",
|
||||
|
87
src/codegen/background-merge-task.h
Normal file
87
src/codegen/background-merge-task.h
Normal 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_
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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_; }
|
||||
|
||||
|
@ -237,4 +237,384 @@ 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);
|
||||
|
||||
// A second round of GC is necessary in case incremental marking had already
|
||||
// started before the bytecode was aged.
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user