Revert^2 "Correctly handle thread deopt with thread-specific JVMTI events"

This reverts commit b2a8964f4218c2c52dacf599ebf5cf69f8753bf0.

It turns out that transitioning from instrumentation trampolines and
interpreter trampolines interacts in a racy way with single thread
deoptimization. This caused tests that perform this transition
repeatedly to be flaky when run with the --trace. Since it is expected
to be rare that one traces at the same time they are performing JVMTI
functions (since JVMTI is a superset of trace behaviors) we solved
this by simply not allowing the transition.

Reason for revert: Prevented unsafe transition between entry-exit
                   trampolines and interpreter only.
Bug: 131865028
Bug: 132283660
Test: ./test.py --host --trace --ntrace
Test: echo "#!/bin/bash" > run-one-test.sh
      echo "./art/test/run-test --dev --jit --trace --64 1956-pop-frame-jit-calling 2>&1" >> run-one-test.sh
      chmod u+x run-one-test.sh
      ./art/tools/parallel_run.py -j20 ./run-one-test.sh

Change-Id: Id496b272f353a5a5e000574c107a97d67405d54b
diff --git a/openjdkjvmti/deopt_manager.cc b/openjdkjvmti/deopt_manager.cc
index a7feba8..3b04ed8 100644
--- a/openjdkjvmti/deopt_manager.cc
+++ b/openjdkjvmti/deopt_manager.cc
@@ -45,6 +45,7 @@
 #include "gc/collector_type.h"
 #include "gc/heap.h"
 #include "gc/scoped_gc_critical_section.h"
+#include "instrumentation.h"
 #include "jit/jit.h"
 #include "jni/jni_internal.h"
 #include "mirror/class-inl.h"
@@ -472,8 +473,12 @@
   deopter_count_++;
   if (deopter_count_ == 1) {
     ScopedDeoptimizationContext sdc(self, this);
-    art::Runtime::Current()->GetInstrumentation()->EnableDeoptimization();
-    return;
+    art::instrumentation::Instrumentation* instrumentation =
+        art::Runtime::Current()->GetInstrumentation();
+    // Enable deoptimization
+    instrumentation->EnableDeoptimization();
+    // Tell instrumentation we will be deopting single threads.
+    instrumentation->EnableSingleThreadDeopt();
   } else {
     deoptimization_status_lock_.ExclusiveUnlock(self);
   }
diff --git a/openjdkjvmti/events.cc b/openjdkjvmti/events.cc
index 16abf41..40e8b80 100644
--- a/openjdkjvmti/events.cc
+++ b/openjdkjvmti/events.cc
@@ -974,6 +974,8 @@
 }
 
 enum class DeoptRequirement {
+  // No deoptimization work required.
+  kNone,
   // Limited/no deopt required.
   kLimited,
   // A single thread must be put into interpret only.
@@ -998,19 +1000,38 @@
     case ArtJvmtiEvent::kSingleStep:
     case ArtJvmtiEvent::kFramePop:
       return thread == nullptr ? DeoptRequirement::kFull : DeoptRequirement::kThread;
-    default:
-      LOG(FATAL) << "Unexpected event type!";
-      UNREACHABLE();
+    case ArtJvmtiEvent::kVmInit:
+    case ArtJvmtiEvent::kVmDeath:
+    case ArtJvmtiEvent::kThreadStart:
+    case ArtJvmtiEvent::kThreadEnd:
+    case ArtJvmtiEvent::kClassFileLoadHookNonRetransformable:
+    case ArtJvmtiEvent::kClassLoad:
+    case ArtJvmtiEvent::kClassPrepare:
+    case ArtJvmtiEvent::kVmStart:
+    case ArtJvmtiEvent::kNativeMethodBind:
+    case ArtJvmtiEvent::kCompiledMethodLoad:
+    case ArtJvmtiEvent::kCompiledMethodUnload:
+    case ArtJvmtiEvent::kDynamicCodeGenerated:
+    case ArtJvmtiEvent::kDataDumpRequest:
+    case ArtJvmtiEvent::kMonitorWait:
+    case ArtJvmtiEvent::kMonitorWaited:
+    case ArtJvmtiEvent::kMonitorContendedEnter:
+    case ArtJvmtiEvent::kMonitorContendedEntered:
+    case ArtJvmtiEvent::kResourceExhausted:
+    case ArtJvmtiEvent::kGarbageCollectionStart:
+    case ArtJvmtiEvent::kGarbageCollectionFinish:
+    case ArtJvmtiEvent::kObjectFree:
+    case ArtJvmtiEvent::kVmObjectAlloc:
+    case ArtJvmtiEvent::kClassFileLoadHookRetransformable:
+    case ArtJvmtiEvent::kDdmPublishChunk:
+      return DeoptRequirement::kNone;
   }
 }
 
-jvmtiError EventHandler::SetupTraceListener(JvmtiMethodTraceListener* listener,
-                                            ArtJvmtiEvent event,
-                                            jthread thread,
-                                            bool enable) {
+jvmtiError EventHandler::HandleEventDeopt(ArtJvmtiEvent event, jthread thread, bool enable) {
   DeoptRequirement deopt_req = GetDeoptRequirement(event, thread);
   // Make sure we can deopt.
-  {
+  if (deopt_req != DeoptRequirement::kNone) {
     art::ScopedObjectAccess soa(art::Thread::Current());
     DeoptManager* deopt_manager = DeoptManager::Get();
     jvmtiError err = OK;
@@ -1047,7 +1068,12 @@
       }
     }
   }
+  return OK;
+}
 
+void EventHandler::SetupTraceListener(JvmtiMethodTraceListener* listener,
+                                      ArtJvmtiEvent event,
+                                      bool enable) {
   // Add the actual listeners.
   uint32_t new_events = GetInstrumentationEventsFor(event);
   if (new_events == art::instrumentation::Instrumentation::kDexPcMoved) {
@@ -1060,7 +1086,7 @@
     if (IsEventEnabledAnywhere(other)) {
       // The event needs to be kept around/is already enabled by the other jvmti event that uses the
       // same instrumentation event.
-      return OK;
+      return;
     }
   }
   art::ScopedThreadStateChange stsc(art::Thread::Current(), art::ThreadState::kNative);
@@ -1071,7 +1097,7 @@
   } else {
     instr->RemoveListener(listener, new_events);
   }
-  return OK;
+  return;
 }
 
 // Makes sure that all compiled methods are AsyncDeoptimizable so we can deoptimize (and force to
@@ -1127,11 +1153,10 @@
   return false;
 }
 
-jvmtiError EventHandler::SetupFramePopTraceListener(jthread thread, bool enable) {
+void EventHandler::SetupFramePopTraceListener(bool enable) {
   if (enable) {
     frame_pop_enabled = true;
-    return SetupTraceListener(
-        method_trace_listener_.get(), ArtJvmtiEvent::kFramePop, thread, enable);
+    SetupTraceListener(method_trace_listener_.get(), ArtJvmtiEvent::kFramePop, enable);
   } else {
     // remove the listener if we have no outstanding frames.
     {
@@ -1140,38 +1165,37 @@
         art::ReaderMutexLock event_mu(art::Thread::Current(), env->event_info_mutex_);
         if (!env->notify_frames.empty()) {
           // Leaving FramePop listener since there are unsent FramePop events.
-          return OK;
+          return;
         }
       }
       frame_pop_enabled = false;
     }
-    return SetupTraceListener(
-        method_trace_listener_.get(), ArtJvmtiEvent::kFramePop, thread, enable);
+    SetupTraceListener(method_trace_listener_.get(), ArtJvmtiEvent::kFramePop, enable);
   }
 }
 
 // Handle special work for the given event type, if necessary.
-jvmtiError EventHandler::HandleEventType(ArtJvmtiEvent event, jthread thread, bool enable) {
+void EventHandler::HandleEventType(ArtJvmtiEvent event, bool enable) {
   switch (event) {
     case ArtJvmtiEvent::kDdmPublishChunk:
       SetupDdmTracking(ddm_listener_.get(), enable);
-      return OK;
+      return;
     case ArtJvmtiEvent::kVmObjectAlloc:
       SetupObjectAllocationTracking(alloc_listener_.get(), enable);
-      return OK;
+      return;
     case ArtJvmtiEvent::kGarbageCollectionStart:
     case ArtJvmtiEvent::kGarbageCollectionFinish:
       SetupGcPauseTracking(gc_pause_listener_.get(), event, enable);
-      return OK;
+      return;
     // FramePop can never be disabled once it's been turned on if it was turned off with outstanding
     // pop-events since we would either need to deal with dangling pointers or have missed events.
     case ArtJvmtiEvent::kFramePop:
       if (enable && frame_pop_enabled) {
         // The frame-pop event was held on by pending events so we don't need to do anything.
-        break;
       } else {
-        return SetupFramePopTraceListener(thread, enable);
+        SetupFramePopTraceListener(enable);
       }
+      return;
     case ArtJvmtiEvent::kMethodEntry:
     case ArtJvmtiEvent::kMethodExit:
     case ArtJvmtiEvent::kFieldAccess:
@@ -1180,7 +1204,8 @@
     case ArtJvmtiEvent::kExceptionCatch:
     case ArtJvmtiEvent::kBreakpoint:
     case ArtJvmtiEvent::kSingleStep:
-      return SetupTraceListener(method_trace_listener_.get(), event, thread, enable);
+      SetupTraceListener(method_trace_listener_.get(), event, enable);
+      return;
     case ArtJvmtiEvent::kMonitorContendedEnter:
     case ArtJvmtiEvent::kMonitorContendedEntered:
     case ArtJvmtiEvent::kMonitorWait:
@@ -1188,11 +1213,11 @@
       if (!OtherMonitorEventsEnabledAnywhere(event)) {
         SetupMonitorListener(monitor_listener_.get(), park_listener_.get(), enable);
       }
-      return OK;
+      return;
     default:
       break;
   }
-  return OK;
+  return;
 }
 
 // Checks to see if the env has the capabilities associated with the given event.
@@ -1276,8 +1301,15 @@
   art::Thread* self = art::Thread::Current();
   art::Thread* target = nullptr;
   ScopedNoUserCodeSuspension snucs(self);
+  // The overall state across all threads and jvmtiEnvs. This is used to control the state of the
+  // instrumentation handlers since we only want each added once.
   bool old_state;
   bool new_state;
+  // The state for just the current 'thread' (including null) across all jvmtiEnvs. This is used to
+  // control the deoptimization state since we do refcounting for that and need to perform different
+  // actions depending on if the event is limited to a single thread or global.
+  bool old_thread_state;
+  bool new_thread_state;
   {
     // From now on we know we cannot get suspended by user-code.
     // NB This does a SuspendCheck (during thread state change) so we need to
@@ -1296,28 +1328,55 @@
       }
     }
 
+
     art::WriterMutexLock ei_mu(self, env->event_info_mutex_);
+    old_thread_state = GetThreadEventState(event, target);
     old_state = global_mask.Test(event);
     if (mode == JVMTI_ENABLE) {
       env->event_masks.EnableEvent(env, target, event);
       global_mask.Set(event);
       new_state = true;
+      new_thread_state = true;
+      DCHECK(GetThreadEventState(event, target));
     } else {
       DCHECK_EQ(mode, JVMTI_DISABLE);
 
       env->event_masks.DisableEvent(env, target, event);
       RecalculateGlobalEventMaskLocked(event);
       new_state = global_mask.Test(event);
+      new_thread_state = GetThreadEventState(event, target);
+      DCHECK(new_state || !new_thread_state);
     }
   }
   // Handle any special work required for the event type. We still have the
   // user_code_suspend_count_lock_ so there won't be any interleaving here.
   if (new_state != old_state) {
-    return HandleEventType(event, thread, mode == JVMTI_ENABLE);
+    HandleEventType(event, mode == JVMTI_ENABLE);
+  }
+  if (old_thread_state != new_thread_state) {
+    return HandleEventDeopt(event, thread, new_thread_state);
   }
   return OK;
 }
 
+bool EventHandler::GetThreadEventState(ArtJvmtiEvent event, art::Thread* thread) {
+  for (ArtJvmTiEnv* stored_env : envs) {
+    if (stored_env == nullptr) {
+      continue;
+    }
+    auto& masks = stored_env->event_masks;
+    if (thread == nullptr && masks.global_event_mask.Test(event)) {
+      return true;
+    } else if (thread != nullptr) {
+      EventMask* mask =  masks.GetEventMaskOrNull(thread);
+      if (mask != nullptr && mask->Test(event)) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
 void EventHandler::HandleBreakpointEventsChanged(bool added) {
   if (added) {
     DeoptManager::Get()->AddDeoptimizationRequester();
diff --git a/openjdkjvmti/events.h b/openjdkjvmti/events.h
index 2c3c7a0..d54c87a 100644
--- a/openjdkjvmti/events.h
+++ b/openjdkjvmti/events.h
@@ -247,13 +247,10 @@
       REQUIRES(!envs_lock_);
 
  private:
-  jvmtiError SetupTraceListener(JvmtiMethodTraceListener* listener,
-                                ArtJvmtiEvent event,
-                                jthread thread,
-                                bool enable);
+  void SetupTraceListener(JvmtiMethodTraceListener* listener, ArtJvmtiEvent event, bool enable);
 
   // Specifically handle the FramePop event which it might not always be possible to turn off.
-  jvmtiError SetupFramePopTraceListener(jthread thread, bool enable);
+  void SetupFramePopTraceListener(bool enable);
 
   template <ArtJvmtiEvent kEvent, typename ...Args>
   ALWAYS_INLINE
@@ -293,6 +290,11 @@
   ALWAYS_INLINE
   inline void RecalculateGlobalEventMaskLocked(ArtJvmtiEvent event) REQUIRES_SHARED(envs_lock_);
 
+  // Returns whether there are any active requests for the given event on the given thread. This
+  // should only be used while modifying the events for a thread.
+  bool GetThreadEventState(ArtJvmtiEvent event, art::Thread* thread)
+      REQUIRES(envs_lock_, art::Locks::thread_list_lock_);
+
   template <ArtJvmtiEvent kEvent>
   ALWAYS_INLINE inline void DispatchClassFileLoadHookEvent(art::Thread* thread,
                                                            JNIEnv* jnienv,
@@ -313,7 +315,11 @@
                                                             jclass klass) const
       REQUIRES(!envs_lock_);
 
-  jvmtiError HandleEventType(ArtJvmtiEvent event, jthread thread, bool enable);
+  // Sets up the global state needed for the first/last enable of an event across all threads
+  void HandleEventType(ArtJvmtiEvent event, bool enable);
+  // Perform deopts required for enabling the event on the given thread. Null thread indicates
+  // global event enabled.
+  jvmtiError HandleEventDeopt(ArtJvmtiEvent event, jthread thread, bool enable);
   void HandleLocalAccessCapabilityAdded();
   void HandleBreakpointEventsChanged(bool enable);
 
diff --git a/runtime/instrumentation.cc b/runtime/instrumentation.cc
index 7d572b6..57f7948 100644
--- a/runtime/instrumentation.cc
+++ b/runtime/instrumentation.cc
@@ -169,7 +169,8 @@
       deoptimization_enabled_(false),
       interpreter_handler_table_(kMainHandlerTable),
       quick_alloc_entry_points_instrumentation_counter_(0),
-      alloc_entrypoints_instrumented_(false) {
+      alloc_entrypoints_instrumented_(false),
+      can_use_instrumentation_trampolines_(true) {
 }
 
 void Instrumentation::InstallStubsForClass(ObjPtr<mirror::Class> klass) {
@@ -698,6 +699,19 @@
   return GetCurrentInstrumentationLevel() != new_level;
 }
 
+void Instrumentation::UpdateInstrumentationLevels(InstrumentationLevel level) {
+  if (level == InstrumentationLevel::kInstrumentWithInterpreter) {
+    can_use_instrumentation_trampolines_ = false;
+  }
+  if (UNLIKELY(!can_use_instrumentation_trampolines_)) {
+    for (auto& p : requested_instrumentation_levels_) {
+      if (p.second == InstrumentationLevel::kInstrumentWithInstrumentationStubs) {
+        p.second = InstrumentationLevel::kInstrumentWithInterpreter;
+      }
+    }
+  }
+}
+
 void Instrumentation::ConfigureStubs(const char* key, InstrumentationLevel desired_level) {
   // Store the instrumentation level for this key or remove it.
   if (desired_level == InstrumentationLevel::kInstrumentNothing) {
@@ -708,12 +722,29 @@
     requested_instrumentation_levels_.Overwrite(key, desired_level);
   }
 
+  UpdateInstrumentationLevels(desired_level);
+  UpdateStubs();
+}
+
+void Instrumentation::EnableSingleThreadDeopt() {
+  // Single-thread deopt only uses interpreter.
+  can_use_instrumentation_trampolines_ = false;
+  UpdateInstrumentationLevels(InstrumentationLevel::kInstrumentWithInterpreter);
+  UpdateStubs();
+}
+
+void Instrumentation::UpdateStubs() {
   // Look for the highest required instrumentation level.
   InstrumentationLevel requested_level = InstrumentationLevel::kInstrumentNothing;
   for (const auto& v : requested_instrumentation_levels_) {
     requested_level = std::max(requested_level, v.second);
   }
 
+  DCHECK(can_use_instrumentation_trampolines_ ||
+         requested_level != InstrumentationLevel::kInstrumentWithInstrumentationStubs)
+      << "Use trampolines: " << can_use_instrumentation_trampolines_ << " level "
+      << requested_level;
+
   interpret_only_ = (requested_level == InstrumentationLevel::kInstrumentWithInterpreter) ||
                     forced_interpret_only_;
 
diff --git a/runtime/instrumentation.h b/runtime/instrumentation.h
index c8ee2a8..fc64c49 100644
--- a/runtime/instrumentation.h
+++ b/runtime/instrumentation.h
@@ -512,6 +512,13 @@
   void InstallStubsForMethod(ArtMethod* method)
       REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(!GetDeoptimizedMethodsLock());
 
+  // Sets up instrumentation to allow single thread deoptimization using ForceInterpreterCount.
+  void EnableSingleThreadDeopt()
+      REQUIRES(Locks::mutator_lock_, Roles::uninterruptible_)
+      REQUIRES(!Locks::thread_list_lock_,
+               !Locks::classlinker_classes_lock_,
+               !GetDeoptimizedMethodsLock());
+
   // Install instrumentation exit stub on every method of the stack of the given thread.
   // This is used by the debugger to cause a deoptimization of the thread's stack after updating
   // local variable(s).
@@ -546,6 +553,15 @@
       REQUIRES(!GetDeoptimizedMethodsLock(),
                !Locks::thread_list_lock_,
                !Locks::classlinker_classes_lock_);
+  void UpdateStubs() REQUIRES(Locks::mutator_lock_, Roles::uninterruptible_)
+      REQUIRES(!GetDeoptimizedMethodsLock(),
+               !Locks::thread_list_lock_,
+               !Locks::classlinker_classes_lock_);
+  void UpdateInstrumentationLevels(InstrumentationLevel level)
+      REQUIRES(Locks::mutator_lock_, Roles::uninterruptible_)
+      REQUIRES(!GetDeoptimizedMethodsLock(),
+               !Locks::thread_list_lock_,
+               !Locks::classlinker_classes_lock_);
 
   void UpdateInterpreterHandlerTable() REQUIRES(Locks::mutator_lock_) {
     /*
@@ -710,6 +726,11 @@
   // alloc_entrypoints_instrumented_ change during suspend points.
   bool alloc_entrypoints_instrumented_;
 
+  // If we can use instrumentation trampolines. After the first time we instrument something with
+  // the interpreter we can no longer use trampolines because it can lead to stack corruption.
+  // TODO Figure out a way to remove the need for this.
+  bool can_use_instrumentation_trampolines_;
+
   friend class InstrumentationTest;  // For GetCurrentInstrumentationLevel and ConfigureStubs.
   friend class InstrumentationStackPopper;  // For popping instrumentation frames.
 
diff --git a/runtime/instrumentation_test.cc b/runtime/instrumentation_test.cc
index d973689..cf5d3ed 100644
--- a/runtime/instrumentation_test.cc
+++ b/runtime/instrumentation_test.cc
@@ -840,7 +840,8 @@
   // Configure stubs with instrumentation stubs.
   CheckConfigureStubs(kClientOneKey,
                       Instrumentation::InstrumentationLevel::kInstrumentWithInstrumentationStubs);
-  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInstrumentationStubs,
+  // Make sure we are still interpreter since going from interpreter->instrumentation is dangerous.
+  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInterpreter,
                         1U);
 
   // Check we can disable instrumentation.
@@ -866,7 +867,7 @@
   // Configure stubs with instrumentation stubs again.
   CheckConfigureStubs(kClientOneKey,
                       Instrumentation::InstrumentationLevel::kInstrumentWithInstrumentationStubs);
-  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInstrumentationStubs,
+  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInterpreter,
                         1U);
 
   // Check we can disable instrumentation.
@@ -970,9 +971,9 @@
   CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInterpreter, 2U);
 
   // 1st client requests instrumentation deactivation but 2nd client still needs
-  // instrumentation stubs.
+  // instrumentation stubs. Since we already got interpreter stubs we need to stay there.
   CheckConfigureStubs(kClientOneKey, Instrumentation::InstrumentationLevel::kInstrumentNothing);
-  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInstrumentationStubs,
+  CHECK_INSTRUMENTATION(Instrumentation::InstrumentationLevel::kInstrumentWithInterpreter,
                         1U);
 
   // 2nd client requests instrumentation deactivation
diff --git a/test/1962-multi-thread-events/expected.txt b/test/1962-multi-thread-events/expected.txt
new file mode 100644
index 0000000..841eb8b
--- /dev/null
+++ b/test/1962-multi-thread-events/expected.txt
@@ -0,0 +1,4 @@
+Events on thread 1:
+	Hit event on T1 Thread
+Events on thread 2:
+	Hit event on T2 Thread
diff --git a/test/1962-multi-thread-events/info.txt b/test/1962-multi-thread-events/info.txt
new file mode 100644
index 0000000..75a47b8
--- /dev/null
+++ b/test/1962-multi-thread-events/info.txt
@@ -0,0 +1,5 @@
+Tests b/131865028
+
+Due to a mistake in the single-thread deopt CL we would incorrectly only update a threads
+deoptimization count if it was the first thread of a particular event type to be activated. This
+could cause events to be missed.
diff --git a/test/1962-multi-thread-events/multi_thread_events.cc b/test/1962-multi-thread-events/multi_thread_events.cc
new file mode 100644
index 0000000..aeb15b0
--- /dev/null
+++ b/test/1962-multi-thread-events/multi_thread_events.cc
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+
+#include "android-base/macros.h"
+
+#include "jni.h"
+#include "jvmti.h"
+#include "scoped_local_ref.h"
+
+// Test infrastructure
+#include "jni_helper.h"
+#include "jvmti_helper.h"
+#include "test_env.h"
+
+namespace art {
+namespace Test1962MultiThreadEvents {
+
+struct BreakpointData {
+  jobject events;
+  jmethodID target;
+};
+void cbMethodEntry(jvmtiEnv* jvmti,
+                   JNIEnv* env,
+                   jthread thread,
+                   jmethodID method,
+                   jboolean was_exception ATTRIBUTE_UNUSED,
+                   jvalue val ATTRIBUTE_UNUSED) {
+  BreakpointData* data = nullptr;
+  if (JvmtiErrorToException(
+          env, jvmti, jvmti->GetThreadLocalStorage(thread, reinterpret_cast<void**>(&data)))) {
+    return;
+  }
+  if (data->target != method) {
+    return;
+  }
+  jclass klass = env->FindClass("art/Test1962");
+  jmethodID handler =
+      env->GetStaticMethodID(klass, "HandleEvent", "(Ljava/lang/Thread;Ljava/util/List;)V");
+  CHECK(data != nullptr);
+  env->CallStaticVoidMethod(klass, handler, thread, data->events);
+}
+
+extern "C" JNIEXPORT void JNICALL Java_art_Test1962_setupTest(JNIEnv* env,
+                                                              jclass klass ATTRIBUTE_UNUSED) {
+  jvmtiCapabilities caps{
+    .can_generate_method_exit_events = 1,
+  };
+  if (JvmtiErrorToException(env, jvmti_env, jvmti_env->AddCapabilities(&caps))) {
+    return;
+  }
+  jvmtiEventCallbacks cb{
+    .MethodExit = cbMethodEntry,
+  };
+  JvmtiErrorToException(env, jvmti_env, jvmti_env->SetEventCallbacks(&cb, sizeof(cb)));
+}
+
+extern "C" JNIEXPORT void JNICALL Java_art_Test1962_setupThread(
+    JNIEnv* env, jclass klass ATTRIBUTE_UNUSED, jthread thr, jobject events, jobject target) {
+  BreakpointData* data = nullptr;
+  if (JvmtiErrorToException(
+          env, jvmti_env, jvmti_env->Allocate(sizeof(*data), reinterpret_cast<uint8_t**>(&data)))) {
+    return;
+  }
+  data->events = env->NewGlobalRef(events);
+  data->target = env->FromReflectedMethod(target);
+  if (JvmtiErrorToException(env, jvmti_env, jvmti_env->SetThreadLocalStorage(thr, data))) {
+    return;
+  }
+  JvmtiErrorToException(
+      env,
+      jvmti_env,
+      jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, thr));
+}
+
+}  // namespace Test1962MultiThreadEvents
+}  // namespace art
diff --git a/test/1962-multi-thread-events/run b/test/1962-multi-thread-events/run
new file mode 100755
index 0000000..c6e62ae
--- /dev/null
+++ b/test/1962-multi-thread-events/run
@@ -0,0 +1,17 @@
+#!/bin/bash
+#
+# Copyright 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+./default-run "$@" --jvmti
diff --git a/test/1962-multi-thread-events/src/Main.java b/test/1962-multi-thread-events/src/Main.java
new file mode 100644
index 0000000..7669e21
--- /dev/null
+++ b/test/1962-multi-thread-events/src/Main.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class Main {
+  public static void main(String[] args) throws Exception {
+    art.Test1962.run();
+  }
+}
diff --git a/test/1962-multi-thread-events/src/art/Test1962.java b/test/1962-multi-thread-events/src/art/Test1962.java
new file mode 100644
index 0000000..b5c2992
--- /dev/null
+++ b/test/1962-multi-thread-events/src/art/Test1962.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package art;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+public class Test1962 {
+  public static void doNothing() {
+    // We set a breakpoint here!
+  }
+  public static void run() throws Exception {
+    doTest();
+  }
+
+  public static void HandleEvent(Thread t, List<String> events) {
+    events.add("Hit event on " + t.getName());
+  }
+
+  public static void doTest() throws Exception {
+    setupTest();
+
+    final int NUM_THREADS = 2;
+    List<String> t1Events = new ArrayList<>();
+    List<String> t2Events = new ArrayList<>();
+    final CountDownLatch threadResumeLatch = new CountDownLatch(1);
+    final CountDownLatch threadStartLatch = new CountDownLatch(NUM_THREADS);
+    Runnable threadRun = () -> {
+      try {
+        threadStartLatch.countDown();
+        threadResumeLatch.await();
+        doNothing();
+      } catch (Exception e) {
+        throw new Error("Failed at something", e);
+      }
+    };
+    Thread T1 = new Thread(threadRun, "T1 Thread");
+    Thread T2 = new Thread(threadRun, "T2 Thread");
+    T1.start();
+    T2.start();
+    // Wait for both threads to have started.
+    threadStartLatch.await();
+    // Tell the threads to notify us when the doNothing method exits
+    Method target = Test1962.class.getDeclaredMethod("doNothing");
+    setupThread(T1, t1Events, target);
+    setupThread(T2, t2Events, target);
+    // Let the threads go.
+    threadResumeLatch.countDown();
+    // Wait for the threads to finish.
+    T1.join();
+    T2.join();
+    // Print out the events each of the threads performed.
+    System.out.println("Events on thread 1:");
+    for (String s : t1Events) {
+      System.out.println("\t" + s);
+    }
+    System.out.println("Events on thread 2:");
+    for (String s : t2Events) {
+      System.out.println("\t" + s);
+    }
+  }
+
+  public static native void setupTest();
+  public static native void setupThread(Thread t, List<String> events, Method target);
+}
diff --git a/test/Android.bp b/test/Android.bp
index 8b40d3c..380a25d 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -293,6 +293,7 @@
         "1951-monitor-enter-no-suspend/raw_monitor.cc",
         "1953-pop-frame/pop_frame.cc",
         "1957-error-ext/lasterror.cc",
+        "1962-multi-thread-events/multi_thread_events.cc",
     ],
     // Use NDK-compatible headers for ctstiagent.
     header_libs: [
@@ -682,6 +683,7 @@
         "1943-suspend-raw-monitor-wait/src/art/Test1943.java",
         "1953-pop-frame/src/art/Test1953.java",
         "1958-transform-try-jit/src/art/Test1958.java",
+        "1962-multi-thread-events/src/art/Test1962.java",
     ],
 }