[vm/concurrency] Avoid O(n) traversal of isolates to find pending deopt

To avoid a O(n) traversal of isolates in order to find pending deopts,
we'll move the pending deopt state to the Thread. It's really a property
of a thread: when the thread's stack gets unwound - via return or
exceptional flow - we remove pending deopts.

This CL also encapsulates the pending deopt logic into a [PendingDeopts]
class - thereby avoiding logic around manipulating this array being
spread around. It simplifies the already very big [Isolate] class.

TEST=Refactoring of existing code.

Issue https://github.com/dart-lang/sdk/issues/36097
Closes https://github.com/dart-lang/sdk/issues/45168

Change-Id: I5a9b18c577f21a0f2a88332b1bc26641ec39d2af
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/188521
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
diff --git a/runtime/vm/exceptions.cc b/runtime/vm/exceptions.cc
index 306c122..363fd1e 100644
--- a/runtime/vm/exceptions.cc
+++ b/runtime/vm/exceptions.cc
@@ -537,44 +537,8 @@
   *handler_fp = frame->fp();
 }
 
-static uword RemapExceptionPCForDeopt(Thread* thread,
-                                      uword program_counter,
-                                      uword frame_pointer) {
-  MallocGrowableArray<PendingLazyDeopt>* pending_deopts =
-      thread->isolate()->pending_deopts();
-  if (pending_deopts->length() > 0) {
-    // Check if the target frame is scheduled for lazy deopt.
-    for (intptr_t i = 0; i < pending_deopts->length(); i++) {
-      if ((*pending_deopts)[i].fp() == frame_pointer) {
-        // Deopt should now resume in the catch handler instead of after the
-        // call.
-        (*pending_deopts)[i].set_pc(program_counter);
-
-        // Jump to the deopt stub instead of the catch handler.
-        program_counter = StubCode::DeoptimizeLazyFromThrow().EntryPoint();
-        if (FLAG_trace_deoptimization) {
-          THR_Print("Throwing to frame scheduled for lazy deopt fp=%" Pp "\n",
-                    frame_pointer);
-
-#if defined(DEBUG)
-          // Ensure the frame references optimized code.
-          ObjectPtr pc_marker = *(reinterpret_cast<ObjectPtr*>(
-              frame_pointer + runtime_frame_layout.code_from_fp * kWordSize));
-          Code& code = Code::Handle(Code::RawCast(pc_marker));
-          ASSERT(code.is_optimized() && !code.is_force_optimized());
-#endif
-        }
-        break;
-      }
-    }
-  }
-  return program_counter;
-}
-
 static void ClearLazyDeopts(Thread* thread, uword frame_pointer) {
-  MallocGrowableArray<PendingLazyDeopt>* pending_deopts =
-      thread->isolate()->pending_deopts();
-  if (pending_deopts->length() > 0) {
+  if (thread->pending_deopts().HasPendingDeopts()) {
     // We may be jumping over frames scheduled for lazy deopt. Remove these
     // frames from the pending deopt table, but only after unmarking them so
     // any stack walk that happens before the stack is unwound will still work.
@@ -596,17 +560,8 @@
     ValidateFrames();
 #endif
 
-    for (intptr_t i = 0; i < pending_deopts->length(); i++) {
-      if ((*pending_deopts)[i].fp() < frame_pointer) {
-        if (FLAG_trace_deoptimization) {
-          THR_Print(
-              "Lazy deopt skipped due to throw for "
-              "fp=%" Pp ", pc=%" Pp "\n",
-              (*pending_deopts)[i].fp(), (*pending_deopts)[i].pc());
-        }
-        pending_deopts->RemoveAt(i--);
-      }
-    }
+    thread->pending_deopts().ClearPendingDeoptsBelow(
+        frame_pointer, PendingDeopts::kClearDueToThrow);
 
 #if defined(DEBUG)
     ValidateFrames();
@@ -620,8 +575,8 @@
                                    uword frame_pointer,
                                    const Object& exception_object,
                                    const Object& stacktrace_object) {
-  uword remapped_pc =
-      RemapExceptionPCForDeopt(thread, program_counter, frame_pointer);
+  uword remapped_pc = thread->pending_deopts().RemapExceptionPCForDeopt(
+      program_counter, frame_pointer);
   thread->set_active_exception(exception_object);
   thread->set_active_stacktrace(stacktrace_object);
   thread->set_resume_pc(remapped_pc);
diff --git a/runtime/vm/heap/weak_code.cc b/runtime/vm/heap/weak_code.cc
index c23372b..c482fe7 100644
--- a/runtime/vm/heap/weak_code.cc
+++ b/runtime/vm/heap/weak_code.cc
@@ -77,15 +77,15 @@
   isolate_group->RunWithStoppedMutators([&]() {
     Code& code = Code::Handle();
     isolate_group->ForEachIsolate([&](Isolate* isolate) {
+      auto mutator_thread = isolate->mutator_thread();
       DartFrameIterator iterator(
-          isolate->mutator_thread(),
-          StackFrameIterator::kAllowCrossThreadIteration);
+          mutator_thread, StackFrameIterator::kAllowCrossThreadIteration);
       StackFrame* frame = iterator.NextFrame();
       while (frame != nullptr) {
         code = frame->LookupDartCode();
         if (IsOptimizedCode(code_objects, code)) {
           ReportDeoptimization(code);
-          DeoptimizeAt(isolate, code, frame);
+          DeoptimizeAt(mutator_thread, code, frame);
         }
         frame = iterator.NextFrame();
       }
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index 46dfb45..0781a1b 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -1731,7 +1731,6 @@
       on_cleanup_callback_(Isolate::CleanupCallback()),
       random_(),
       mutex_(NOT_IN_PRODUCT("Isolate::mutex_")),
-      pending_deopts_(new MallocGrowableArray<PendingLazyDeopt>()),
       tag_table_(GrowableObjectArray::null()),
       sticky_error_(Error::null()),
       spawn_count_monitor_(),
@@ -1776,8 +1775,6 @@
 #if defined(USING_SIMULATOR)
   delete simulator_;
 #endif
-  delete pending_deopts_;
-  pending_deopts_ = nullptr;
   delete message_handler_;
   message_handler_ =
       nullptr;  // Fail fast if we send messages to a dead isolate.
@@ -2857,18 +2854,6 @@
   api_state()->VisitWeakHandlesUnlocked(visitor);
 }
 
-uword IsolateGroup::FindPendingDeoptAtSafepoint(uword fp) {
-  for (Isolate* isolate : isolates_) {
-    for (intptr_t i = 0; i < isolate->pending_deopts_->length(); i++) {
-      if ((*isolate->pending_deopts_)[i].fp() == fp) {
-        return (*isolate->pending_deopts_)[i].pc();
-      }
-    }
-  }
-  FATAL("Missing pending deopt entry");
-  return 0;
-}
-
 void IsolateGroup::DeferredMarkLiveTemporaries() {
   ForEachIsolate(
       [&](Isolate* isolate) { isolate->DeferredMarkLiveTemporaries(); },
@@ -2917,42 +2902,6 @@
 }
 #endif  // !defined(PRODUCT)
 
-void Isolate::AddPendingDeopt(uword fp, uword pc) {
-  // GrowableArray::Add is not atomic and may be interrupt by a profiler
-  // stack walk.
-  MallocGrowableArray<PendingLazyDeopt>* old_pending_deopts = pending_deopts_;
-  MallocGrowableArray<PendingLazyDeopt>* new_pending_deopts =
-      new MallocGrowableArray<PendingLazyDeopt>(old_pending_deopts->length() +
-                                                1);
-  for (intptr_t i = 0; i < old_pending_deopts->length(); i++) {
-    ASSERT((*old_pending_deopts)[i].fp() != fp);
-    new_pending_deopts->Add((*old_pending_deopts)[i]);
-  }
-  PendingLazyDeopt deopt(fp, pc);
-  new_pending_deopts->Add(deopt);
-
-  pending_deopts_ = new_pending_deopts;
-  delete old_pending_deopts;
-}
-
-uword Isolate::FindPendingDeopt(uword fp) const {
-  for (intptr_t i = 0; i < pending_deopts_->length(); i++) {
-    if ((*pending_deopts_)[i].fp() == fp) {
-      return (*pending_deopts_)[i].pc();
-    }
-  }
-  FATAL("Missing pending deopt entry");
-  return 0;
-}
-
-void Isolate::ClearPendingDeoptsAtOrBelow(uword fp) const {
-  for (intptr_t i = pending_deopts_->length() - 1; i >= 0; i--) {
-    if ((*pending_deopts_)[i].fp() <= fp) {
-      pending_deopts_->RemoveAt(i);
-    }
-  }
-}
-
 #ifndef PRODUCT
 static const char* ExceptionPauseInfoToServiceEnum(Dart_ExceptionPauseInfo pi) {
   switch (pi) {
diff --git a/runtime/vm/isolate.h b/runtime/vm/isolate.h
index fc85824..c8217eb 100644
--- a/runtime/vm/isolate.h
+++ b/runtime/vm/isolate.h
@@ -96,18 +96,6 @@
 constexpr int kNullSafetyOptionStrong = 2;
 extern int FLAG_sound_null_safety;
 
-class PendingLazyDeopt {
- public:
-  PendingLazyDeopt(uword fp, uword pc) : fp_(fp), pc_(pc) {}
-  uword fp() { return fp_; }
-  uword pc() { return pc_; }
-  void set_pc(uword pc) { pc_ = pc; }
-
- private:
-  uword fp_;
-  uword pc_;
-};
-
 class IsolateVisitor {
  public:
   IsolateVisitor() {}
@@ -776,8 +764,6 @@
         RemappingCidsBit::update(value, isolate_group_flags_);
   }
 
-  uword FindPendingDeoptAtSafepoint(uword fp);
-
   void RememberLiveTemporaries();
   void DeferredMarkLiveTemporaries();
 
@@ -1230,12 +1216,6 @@
   ObjectIdRing* EnsureObjectIdRing();
 #endif  // !defined(PRODUCT)
 
-  void AddPendingDeopt(uword fp, uword pc);
-  uword FindPendingDeopt(uword fp) const;
-  void ClearPendingDeoptsAtOrBelow(uword fp) const;
-  MallocGrowableArray<PendingLazyDeopt>* pending_deopts() const {
-    return pending_deopts_;
-  }
   bool IsDeoptimizing() const { return deopt_context_ != nullptr; }
   DeoptContext* deopt_context() const { return deopt_context_; }
   void set_deopt_context(DeoptContext* value) {
@@ -1618,7 +1598,6 @@
   Mutex mutex_;                            // Protects compiler stats.
   MessageHandler* message_handler_ = nullptr;
   intptr_t defer_finalization_count_ = 0;
-  MallocGrowableArray<PendingLazyDeopt>* pending_deopts_;
   DeoptContext* deopt_context_ = nullptr;
 
   GrowableObjectArrayPtr tag_table_;
diff --git a/runtime/vm/pending_deopts.cc b/runtime/vm/pending_deopts.cc
new file mode 100644
index 0000000..46ae036
--- /dev/null
+++ b/runtime/vm/pending_deopts.cc
@@ -0,0 +1,104 @@
+// Copyright (c) 2021, 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/pending_deopts.h"
+#include "vm/log.h"
+#include "vm/stack_frame.h"
+#include "vm/stub_code.h"
+
+namespace dart {
+
+DECLARE_FLAG(bool, trace_deoptimization);
+
+PendingDeopts::PendingDeopts()
+    : pending_deopts_(new MallocGrowableArray<PendingLazyDeopt>()) {}
+PendingDeopts::~PendingDeopts() {
+  delete pending_deopts_;
+  pending_deopts_ = nullptr;
+}
+
+void PendingDeopts::AddPendingDeopt(uword fp, uword pc) {
+  // GrowableArray::Add is not atomic and may be interrupted by a profiler
+  // stack walk.
+  MallocGrowableArray<PendingLazyDeopt>* old_pending_deopts = pending_deopts_;
+  MallocGrowableArray<PendingLazyDeopt>* new_pending_deopts =
+      new MallocGrowableArray<PendingLazyDeopt>(old_pending_deopts->length() +
+                                                1);
+  for (intptr_t i = 0; i < old_pending_deopts->length(); i++) {
+    ASSERT((*old_pending_deopts)[i].fp() != fp);
+    new_pending_deopts->Add((*old_pending_deopts)[i]);
+  }
+  PendingLazyDeopt deopt(fp, pc);
+  new_pending_deopts->Add(deopt);
+
+  pending_deopts_ = new_pending_deopts;
+  delete old_pending_deopts;
+}
+
+uword PendingDeopts::FindPendingDeopt(uword fp) {
+  for (intptr_t i = 0; i < pending_deopts_->length(); i++) {
+    if ((*pending_deopts_)[i].fp() == fp) {
+      return (*pending_deopts_)[i].pc();
+    }
+  }
+  FATAL("Missing pending deopt entry");
+  return 0;
+}
+
+void PendingDeopts::ClearPendingDeoptsBelow(uword fp, ClearReason reason) {
+  for (intptr_t i = pending_deopts_->length() - 1; i >= 0; i--) {
+    if ((*pending_deopts_)[i].fp() < fp) {
+      if (FLAG_trace_deoptimization) {
+        switch (reason) {
+          case kClearDueToThrow:
+            THR_Print(
+                "Lazy deopt skipped due to throw for "
+                "fp=%" Pp ", pc=%" Pp "\n",
+                (*pending_deopts_)[i].fp(), (*pending_deopts_)[i].pc());
+            break;
+          case kClearDueToDeopt:
+            THR_Print("Lazy deopt fp=%" Pp " pc=%" Pp "\n",
+                      (*pending_deopts_)[i].fp(), (*pending_deopts_)[i].pc());
+            break;
+        }
+      }
+      pending_deopts_->RemoveAt(i);
+    }
+  }
+}
+
+void PendingDeopts::ClearPendingDeoptsAtOrBelow(uword fp, ClearReason reason) {
+  ClearPendingDeoptsBelow(fp + kWordSize, reason);
+}
+
+uword PendingDeopts::RemapExceptionPCForDeopt(uword program_counter,
+                                              uword frame_pointer) {
+  // Check if the target frame is scheduled for lazy deopt.
+  for (intptr_t i = 0; i < pending_deopts_->length(); i++) {
+    if ((*pending_deopts_)[i].fp() == frame_pointer) {
+      // Deopt should now resume in the catch handler instead of after the
+      // call.
+      (*pending_deopts_)[i].set_pc(program_counter);
+
+      // Jump to the deopt stub instead of the catch handler.
+      program_counter = StubCode::DeoptimizeLazyFromThrow().EntryPoint();
+      if (FLAG_trace_deoptimization) {
+        THR_Print("Throwing to frame scheduled for lazy deopt fp=%" Pp "\n",
+                  frame_pointer);
+
+#if defined(DEBUG)
+        // Ensure the frame references optimized code.
+        ObjectPtr pc_marker = *(reinterpret_cast<ObjectPtr*>(
+            frame_pointer + runtime_frame_layout.code_from_fp * kWordSize));
+        Code& code = Code::Handle(Code::RawCast(pc_marker));
+        ASSERT(code.is_optimized() && !code.is_force_optimized());
+#endif
+      }
+      break;
+    }
+  }
+  return program_counter;
+}
+
+}  // namespace dart
diff --git a/runtime/vm/pending_deopts.h b/runtime/vm/pending_deopts.h
new file mode 100644
index 0000000..778dba2
--- /dev/null
+++ b/runtime/vm/pending_deopts.h
@@ -0,0 +1,51 @@
+// Copyright (c) 2021, 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_PENDING_DEOPTS_H_
+#define RUNTIME_VM_PENDING_DEOPTS_H_
+
+#if defined(SHOULD_NOT_INCLUDE_RUNTIME)
+#error "Should not include runtime"
+#endif
+
+#include "vm/growable_array.h"
+
+namespace dart {
+
+class PendingLazyDeopt {
+ public:
+  PendingLazyDeopt(uword fp, uword pc) : fp_(fp), pc_(pc) {}
+  uword fp() { return fp_; }
+  uword pc() { return pc_; }
+  void set_pc(uword pc) { pc_ = pc; }
+
+ private:
+  uword fp_;
+  uword pc_;
+};
+
+class PendingDeopts {
+ public:
+  enum ClearReason {
+    kClearDueToThrow,
+    kClearDueToDeopt,
+  };
+  PendingDeopts();
+  ~PendingDeopts();
+
+  bool HasPendingDeopts() { return pending_deopts_->length() > 0; }
+
+  void AddPendingDeopt(uword fp, uword pc);
+  uword FindPendingDeopt(uword fp);
+  void ClearPendingDeoptsBelow(uword fp, ClearReason reason);
+  void ClearPendingDeoptsAtOrBelow(uword fp, ClearReason reason);
+  uword RemapExceptionPCForDeopt(uword program_counter, uword frame_pointer);
+
+ private:
+  MallocGrowableArray<PendingLazyDeopt>* pending_deopts_;
+};
+
+}  // namespace dart
+
+#endif  // RUNTIME_VM_PENDING_DEOPTS_H_
diff --git a/runtime/vm/runtime_entry.cc b/runtime/vm/runtime_entry.cc
index bf4bffa..b3325b0 100644
--- a/runtime/vm/runtime_entry.cc
+++ b/runtime/vm/runtime_entry.cc
@@ -2931,7 +2931,7 @@
   }
 }
 
-void DeoptimizeAt(Isolate* isolate,
+void DeoptimizeAt(Thread* mutator_thread,
                   const Code& optimized_code,
                   StackFrame* frame) {
   ASSERT(optimized_code.is_optimized());
@@ -2974,7 +2974,7 @@
 
     // N.B.: Update the pending deopt table before updating the frame. The
     // profiler may attempt a stack walk in between.
-    isolate->AddPendingDeopt(frame->fp(), deopt_pc);
+    mutator_thread->pending_deopts().AddPendingDeopt(frame->fp(), deopt_pc);
     frame->MarkForLazyDeopt();
 
     if (FLAG_trace_deoptimization) {
@@ -2994,15 +2994,15 @@
   isolate_group->RunWithStoppedMutators([&]() {
     Code& optimized_code = Code::Handle();
     isolate_group->ForEachIsolate([&](Isolate* isolate) {
+      auto mutator_thread = isolate->mutator_thread();
       DartFrameIterator iterator(
-          isolate->mutator_thread(),
-          StackFrameIterator::kAllowCrossThreadIteration);
+          mutator_thread, StackFrameIterator::kAllowCrossThreadIteration);
       StackFrame* frame = iterator.NextFrame();
       while (frame != nullptr) {
         optimized_code = frame->LookupDartCode();
         if (optimized_code.is_optimized() &&
             !optimized_code.is_force_optimized()) {
-          DeoptimizeAt(isolate, optimized_code, frame);
+          DeoptimizeAt(mutator_thread, optimized_code, frame);
         }
         frame = iterator.NextFrame();
       }
@@ -3090,18 +3090,16 @@
   }
 
   if (is_lazy_deopt != 0u) {
-    uword deopt_pc = isolate->FindPendingDeopt(caller_frame->fp());
-    if (FLAG_trace_deoptimization) {
-      THR_Print("Lazy deopt fp=%" Pp " pc=%" Pp "\n", caller_frame->fp(),
-                deopt_pc);
-    }
+    const uword deopt_pc =
+        thread->pending_deopts().FindPendingDeopt(caller_frame->fp());
 
     // N.B.: Update frame before updating pending deopt table. The profiler
     // may attempt a stack walk in between.
     caller_frame->set_pc(deopt_pc);
     ASSERT(caller_frame->pc() == deopt_pc);
     ASSERT(optimized_code.ContainsInstructionAt(caller_frame->pc()));
-    isolate->ClearPendingDeoptsAtOrBelow(caller_frame->fp());
+    thread->pending_deopts().ClearPendingDeoptsAtOrBelow(
+        caller_frame->fp(), PendingDeopts::kClearDueToDeopt);
   } else {
     if (FLAG_trace_deoptimization) {
       THR_Print("Eager deopt fp=%" Pp " pc=%" Pp "\n", caller_frame->fp(),
diff --git a/runtime/vm/runtime_entry.h b/runtime/vm/runtime_entry.h
index f1171a6..c3468ba 100644
--- a/runtime/vm/runtime_entry.h
+++ b/runtime/vm/runtime_entry.h
@@ -160,7 +160,7 @@
 
 const char* DeoptReasonToCString(ICData::DeoptReasonId deopt_reason);
 
-void DeoptimizeAt(Isolate* isolate,
+void DeoptimizeAt(Thread* mutator_thread,
                   const Code& optimized_code,
                   StackFrame* frame);
 void DeoptimizeFunctionsOnStack();
diff --git a/runtime/vm/stack_frame.h b/runtime/vm/stack_frame.h
index 6969642..2275811 100644
--- a/runtime/vm/stack_frame.h
+++ b/runtime/vm/stack_frame.h
@@ -143,7 +143,7 @@
         fp() + (kSavedCallerPcSlotFromFp * kWordSize)));
     ASSERT(raw_pc != StubCode::DeoptimizeLazyFromThrow().EntryPoint());
     if (raw_pc == StubCode::DeoptimizeLazyFromReturn().EntryPoint()) {
-      return isolate_group()->FindPendingDeoptAtSafepoint(GetCallerFp());
+      return thread_->pending_deopts().FindPendingDeopt(GetCallerFp());
     }
     return raw_pc;
   }
diff --git a/runtime/vm/thread.h b/runtime/vm/thread.h
index be7244a..5aee930 100644
--- a/runtime/vm/thread.h
+++ b/runtime/vm/thread.h
@@ -20,6 +20,7 @@
 #include "vm/handles.h"
 #include "vm/heap/pointer_block.h"
 #include "vm/os_thread.h"
+#include "vm/pending_deopts.h"
 #include "vm/random.h"
 #include "vm/runtime_entry_list.h"
 #include "vm/thread_stack_resource.h"
@@ -908,6 +909,8 @@
   void PrintJSON(JSONStream* stream) const;
 #endif
 
+  PendingDeopts& pending_deopts() { return pending_deopts_; }
+
  private:
   template <class T>
   T* AllocateReusableHandle();
@@ -1012,6 +1015,9 @@
   uint16_t deferred_interrupts_;
   int32_t stack_overflow_count_;
 
+  // Deoptimization of stack frames.
+  PendingDeopts pending_deopts_;
+
   // Compiler state:
   CompilerState* compiler_state_ = nullptr;
   HierarchyInfo* hierarchy_info_;
diff --git a/runtime/vm/vm_sources.gni b/runtime/vm/vm_sources.gni
index a142449..a2ed92d 100644
--- a/runtime/vm/vm_sources.gni
+++ b/runtime/vm/vm_sources.gni
@@ -215,6 +215,8 @@
   "os_win.cc",
   "parser.cc",
   "parser.h",
+  "pending_deopts.cc",
+  "pending_deopts.h",
   "pointer_tagging.h",
   "port.cc",
   "port.h",