dart-sdk/runtime/vm/field_table.h
Martin Kustermann 91c628d63a [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

109 lines
3 KiB
C++

// 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.
#ifndef RUNTIME_VM_FIELD_TABLE_H_
#define RUNTIME_VM_FIELD_TABLE_H_
#include "platform/assert.h"
#include "platform/atomic.h"
#include "vm/bitfield.h"
#include "vm/class_id.h"
#include "vm/globals.h"
#include "vm/growable_array.h"
#include "vm/tagged_pointer.h"
namespace dart {
class Isolate;
class Field;
class FieldInvalidator;
class FieldTable {
public:
explicit FieldTable(Isolate* isolate)
: top_(0),
capacity_(0),
free_head_(-1),
table_(nullptr),
old_tables_(new MallocGrowableArray<InstancePtr*>()),
isolate_(isolate),
is_ready_to_use_(isolate == nullptr) {}
~FieldTable();
bool IsReadyToUse() const;
void MarkReadyToUse();
intptr_t NumFieldIds() const { return top_; }
intptr_t Capacity() const { return capacity_; }
InstancePtr* table() { return table_; }
void FreeOldTables();
// Used by the generated code.
static intptr_t FieldOffsetFor(intptr_t field_id);
bool IsValidIndex(intptr_t index) const { return index >= 0 && index < top_; }
// Returns whether registering this field caused a growth in the backing
// store.
bool Register(const Field& field, intptr_t expected_field_id = -1);
void AllocateIndex(intptr_t index);
// Static field elements are being freed only during isolate reload
// when initially created static field have to get remapped to point
// to an existing static field value.
void Free(intptr_t index);
InstancePtr At(intptr_t index) const {
ASSERT(IsValidIndex(index));
return table_[index];
}
void SetAt(intptr_t index, InstancePtr raw_instance);
FieldTable* Clone(Isolate* for_isolate);
void VisitObjectPointers(ObjectPointerVisitor* visitor);
static const int kInitialCapacity = 512;
static const int kCapacityIncrement = 256;
private:
friend class GCMarker;
friend class MarkingWeakVisitor;
friend class Scavenger;
friend class ScavengerWeakVisitor;
void Grow(intptr_t new_capacity);
intptr_t top_;
intptr_t capacity_;
// -1 if free list is empty, otherwise index of first empty element. Empty
// elements are organized into linked list - they contain index of next
// element, last element contains -1.
intptr_t free_head_;
InstancePtr* table_;
// When table_ grows and have to reallocated, keep the old one here
// so it will get freed when its are no longer in use.
MallocGrowableArray<InstancePtr*>* old_tables_;
// If non-NULL, it will specify the isolate this field table belongs to.
// Growing the field table will keep the cached field table on the isolate's
// mutator thread up-to-date.
Isolate* isolate_;
// Whether this field table is ready to use by e.g. registering new static
// fields.
bool is_ready_to_use_ = false;
DISALLOW_COPY_AND_ASSIGN(FieldTable);
};
} // namespace dart
#endif // RUNTIME_VM_FIELD_TABLE_H_