dart-sdk/runtime/vm/field_table.cc

155 lines
4.5 KiB
C++
Raw Normal View History

// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "vm/field_table.h"
#include "platform/atomic.h"
#include "vm/flags.h"
#include "vm/growable_array.h"
#include "vm/heap/heap.h"
#include "vm/object.h"
#include "vm/object_graph.h"
#include "vm/object_store.h"
#include "vm/raw_object.h"
#include "vm/visitor.h"
namespace dart {
FieldTable::~FieldTable() {
FreeOldTables();
delete old_tables_; // Allocated in FieldTable::FieldTable()
free(table_); // Allocated in FieldTable::Grow()
}
bool FieldTable::IsReadyToUse() const {
DEBUG_ASSERT(
[vm/concurrency] Final support for hot-reload of multi-isolate groups This is the initial implementation of hot reload with multi-isolate groups. Implementation: As before, when a service API call triggers a reload it will be routed as an OOB message to a specific isolate (**). As opposed to before, that isolate has now to coordinate with all other isolates, ensuring that it "owns" the reload and all other isolates are waiting in a state that allows reload. This is implemented as a [ReloadOperationScope] which first participates in other reloads (if there are any) and then owns the reload. It will send a new kind of service message to all other registered isolates. All of them have to check in before reload can proceed. If a new isolate is about to join the group, it will participate when registering the isolate. If an old isolate wants to die, it will participate when unregistering the isolate. This means that in addition to the existing StackOverFlow checks that can process OOB messages and therefore reload, we'll have isolate registration and unregistration as well as a new Isolate::kCheckForReload OOB message handler where an isolate can participate in a reload. We consider the isolate group to be reloadable if the main isolate has loaded the program and set the root library. Helper isolates don't need to load any more kernel code and only initialize core libraries, so it's fine to reload them during this time. (**) The reason we continue to send reload service API calls to any isolate in an isolate group is that re-loading might involve calling out to the embedder's tag handler. Doing so currently requires an active isolate. If we allowed a subset of dart_api.h (the subset needed by the tag handler) to be used only with an active IsolateGroup instead of an active Isolate we could remove this requirement. Edge cases: There's various edge cases to consider: The main edge case is, we currently maintain an upper limit to the number of isolates executing in parallel (to ensure each can have big enough chunk of new space, i.e. TLAB). If there are more isolates with active work they are waiting until one of the exiting ones "yields". To ensure progress, if any such actively running isolate gets a request to participate in a reload, it will mark its own thread as "blocked" and therefore "yields", so another isolate can make progress until all isolates are participating and the reload can start. Marking an isolate as "blocked" happens by exiting that isolate. It will free up it's TLAB, decrease active mutator count and (if running on VM's thread pool) also temporarily increase the thread pool size. The side-effect of this is that it will use one pthread per isolate during reload. In the future we can extend this first implementation, by specially handling isolates that don't have a message handler running. Doing so would require careful consideration to avoid races. Testing: In order to test this we use a small helper framework for reload tests. The helper framework will, similar to real world reload e.g. in flutter, will spawn a subprocess. It will use the service API to trigger reloads in this subproces. To synchronize between the reload driver and the application being reloaded it allows watching for events to be printed to stdout/stderr. The reload test itself can be written - similar to multitests - with annotations such as `// @include-in-relload-0` in them. The testing framework will then generate multiple application versions that all get compiled to kernel. For simplicity we generate the kernel using the standalone VM with `--snapshot-kind=kernel` and avoid using the incremental compiler. There are 4 different tests exercising different aspects of multi-isolate reload: vm/dart_2/isolates/reload_active_stack_test: Performs a reload while a fixed number of isolates have an active stack, thereby ensuring e.g. that all frames of all isolate mutator stacks get deoptimized, ... vm/dart_2/isolates/reload_no_active_stack_test: Similar to the test above, but instead of having an active stack the isolates can yield to the event loop, possibly be even descheduled vm/dart_2/isolates/reload_many_isolates_test: Similar to the test above, but this test uses many more isolates. vm/dart_2/isolates/reload_many_isolates_live_and_die_test: Performs a reload where isolates get spawned and die all the time. There are always P isolates alive at any given point in time, each of them spawns children when their parent has died. Performing a reload catches isolates as various stages of their lifecycle and can therefore cover a lot of corner cases. TEST=vm/dart_2/isolates/reload_*_test.dart Issue https://github.com/dart-lang/sdk/issues/36097 Change-Id: I97039b4084de040b7f2e22f5832a40d57ba398d5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/187461 Commit-Queue: Martin Kustermann <kustermann@google.com> Reviewed-by: Alexander Aprelev <aam@google.com>
2021-03-02 18:57:02 +00:00
IsolateGroup::Current()->IsReloading() ||
IsolateGroup::Current()->program_lock()->IsCurrentThreadReader());
return is_ready_to_use_;
}
void FieldTable::MarkReadyToUse() {
// The isolate will mark it's field table ready-to-use upon initialization of
// the isolate. Only after it was marked as ready-to-use will it participate
// in new static field registrations.
//
// By requiring a read lock here we ensure no other thread is is registering a
// new static field at this moment (it would need exlusive writer lock).
DEBUG_ASSERT(
IsolateGroup::Current()->program_lock()->IsCurrentThreadReader());
ASSERT(!is_ready_to_use_);
is_ready_to_use_ = true;
}
void FieldTable::FreeOldTables() {
while (old_tables_->length() > 0) {
free(old_tables_->RemoveLast());
}
}
intptr_t FieldTable::FieldOffsetFor(intptr_t field_id) {
return field_id * sizeof(ObjectPtr); // NOLINT
}
bool FieldTable::Register(const Field& field, intptr_t expected_field_id) {
DEBUG_ASSERT(
IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter());
ASSERT(is_ready_to_use_);
if (free_head_ < 0) {
bool grown_backing_store = false;
if (top_ == capacity_) {
const intptr_t new_capacity = capacity_ + kCapacityIncrement;
Grow(new_capacity);
grown_backing_store = true;
}
ASSERT(top_ < capacity_);
ASSERT(expected_field_id == -1 || expected_field_id == top_);
field.set_field_id(top_);
table_[top_] = Object::sentinel().ptr();
++top_;
return grown_backing_store;
}
// Reuse existing free element. This is "slow path" that should only be
// triggered after hot reload.
intptr_t reused_free = free_head_;
free_head_ = Smi::Value(Smi::RawCast(table_[free_head_]));
field.set_field_id(reused_free);
table_[reused_free] = Object::sentinel().ptr();
return false;
}
void FieldTable::Free(intptr_t field_id) {
table_[field_id] = Smi::New(free_head_);
free_head_ = field_id;
}
void FieldTable::AllocateIndex(intptr_t index) {
if (index >= capacity_) {
const intptr_t new_capacity = index + kCapacityIncrement;
Grow(new_capacity);
}
ASSERT(table_[index] == ObjectPtr());
if (index >= top_) {
top_ = index + 1;
}
}
void FieldTable::Grow(intptr_t new_capacity) {
ASSERT(new_capacity > capacity_);
auto old_table = table_;
auto new_table = static_cast<ObjectPtr*>(
malloc(new_capacity * sizeof(ObjectPtr))); // NOLINT
intptr_t i;
for (i = 0; i < top_; i++) {
new_table[i] = old_table[i];
}
for (; i < new_capacity; i++) {
new_table[i] = ObjectPtr();
}
capacity_ = new_capacity;
old_tables_->Add(old_table);
// Ensure that new_table_ is populated before it is published
// via store to table_.
reinterpret_cast<AcqRelAtomic<ObjectPtr*>*>(&table_)->store(new_table);
if (isolate_ != nullptr) {
isolate_->mutator_thread()->field_table_values_ = table_;
}
}
FieldTable* FieldTable::Clone(Isolate* for_isolate) {
DEBUG_ASSERT(
IsolateGroup::Current()->program_lock()->IsCurrentThreadReader());
FieldTable* clone = new FieldTable(for_isolate);
auto new_table =
static_cast<ObjectPtr*>(malloc(capacity_ * sizeof(ObjectPtr))); // NOLINT
memmove(new_table, table_, capacity_ * sizeof(ObjectPtr));
ASSERT(clone->table_ == nullptr);
clone->table_ = new_table;
clone->capacity_ = capacity_;
clone->top_ = top_;
[vm/concurrency] Final support for hot-reload of multi-isolate groups This is the initial implementation of hot reload with multi-isolate groups. Implementation: As before, when a service API call triggers a reload it will be routed as an OOB message to a specific isolate (**). As opposed to before, that isolate has now to coordinate with all other isolates, ensuring that it "owns" the reload and all other isolates are waiting in a state that allows reload. This is implemented as a [ReloadOperationScope] which first participates in other reloads (if there are any) and then owns the reload. It will send a new kind of service message to all other registered isolates. All of them have to check in before reload can proceed. If a new isolate is about to join the group, it will participate when registering the isolate. If an old isolate wants to die, it will participate when unregistering the isolate. This means that in addition to the existing StackOverFlow checks that can process OOB messages and therefore reload, we'll have isolate registration and unregistration as well as a new Isolate::kCheckForReload OOB message handler where an isolate can participate in a reload. We consider the isolate group to be reloadable if the main isolate has loaded the program and set the root library. Helper isolates don't need to load any more kernel code and only initialize core libraries, so it's fine to reload them during this time. (**) The reason we continue to send reload service API calls to any isolate in an isolate group is that re-loading might involve calling out to the embedder's tag handler. Doing so currently requires an active isolate. If we allowed a subset of dart_api.h (the subset needed by the tag handler) to be used only with an active IsolateGroup instead of an active Isolate we could remove this requirement. Edge cases: There's various edge cases to consider: The main edge case is, we currently maintain an upper limit to the number of isolates executing in parallel (to ensure each can have big enough chunk of new space, i.e. TLAB). If there are more isolates with active work they are waiting until one of the exiting ones "yields". To ensure progress, if any such actively running isolate gets a request to participate in a reload, it will mark its own thread as "blocked" and therefore "yields", so another isolate can make progress until all isolates are participating and the reload can start. Marking an isolate as "blocked" happens by exiting that isolate. It will free up it's TLAB, decrease active mutator count and (if running on VM's thread pool) also temporarily increase the thread pool size. The side-effect of this is that it will use one pthread per isolate during reload. In the future we can extend this first implementation, by specially handling isolates that don't have a message handler running. Doing so would require careful consideration to avoid races. Testing: In order to test this we use a small helper framework for reload tests. The helper framework will, similar to real world reload e.g. in flutter, will spawn a subprocess. It will use the service API to trigger reloads in this subproces. To synchronize between the reload driver and the application being reloaded it allows watching for events to be printed to stdout/stderr. The reload test itself can be written - similar to multitests - with annotations such as `// @include-in-relload-0` in them. The testing framework will then generate multiple application versions that all get compiled to kernel. For simplicity we generate the kernel using the standalone VM with `--snapshot-kind=kernel` and avoid using the incremental compiler. There are 4 different tests exercising different aspects of multi-isolate reload: vm/dart_2/isolates/reload_active_stack_test: Performs a reload while a fixed number of isolates have an active stack, thereby ensuring e.g. that all frames of all isolate mutator stacks get deoptimized, ... vm/dart_2/isolates/reload_no_active_stack_test: Similar to the test above, but instead of having an active stack the isolates can yield to the event loop, possibly be even descheduled vm/dart_2/isolates/reload_many_isolates_test: Similar to the test above, but this test uses many more isolates. vm/dart_2/isolates/reload_many_isolates_live_and_die_test: Performs a reload where isolates get spawned and die all the time. There are always P isolates alive at any given point in time, each of them spawns children when their parent has died. Performing a reload catches isolates as various stages of their lifecycle and can therefore cover a lot of corner cases. TEST=vm/dart_2/isolates/reload_*_test.dart Issue https://github.com/dart-lang/sdk/issues/36097 Change-Id: I97039b4084de040b7f2e22f5832a40d57ba398d5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/187461 Commit-Queue: Martin Kustermann <kustermann@google.com> Reviewed-by: Alexander Aprelev <aam@google.com>
2021-03-02 18:57:02 +00:00
clone->free_head_ = free_head_;
return clone;
}
void FieldTable::VisitObjectPointers(ObjectPointerVisitor* visitor) {
// GC might try to visit field table before it's isolate done setting it up.
if (table_ == nullptr) {
return;
}
ASSERT(visitor != NULL);
visitor->set_gc_root_type("static fields table");
visitor->VisitPointers(&table_[0], &table_[top_ - 1]);
visitor->clear_gc_root_type();
}
} // namespace dart