JIT mini-debug-info: Remove global maps.

Keep the extra bookkeeping information in JITCodeEntry.

Also do the compression eagerly during GC rather then lazily.

Test: test.py -b --host --jit
Bug: 119800099
Change-Id: Ie6cc682033a32c01d4c2cac242d8a4201116f940
diff --git a/compiler/debug/elf_debug_writer.cc b/compiler/debug/elf_debug_writer.cc
index da3230f..cf7254b 100644
--- a/compiler/debug/elf_debug_writer.cc
+++ b/compiler/debug/elf_debug_writer.cc
@@ -34,6 +34,7 @@
 #include "elf/elf_debug_reader.h"
 #include "elf/elf_utils.h"
 #include "elf/xz_utils.h"
+#include "jit/debugger_interface.h"
 #include "oat.h"
 #include "stream/vector_output_stream.h"
 
@@ -227,15 +228,14 @@
 
 // Combine several mini-debug-info ELF files into one, while filtering some symbols.
 std::vector<uint8_t> PackElfFileForJIT(
-    InstructionSet isa,
-    const InstructionSetFeatures* features ATTRIBUTE_UNUSED,
-    std::vector<ArrayRef<const uint8_t>>& added_elf_files,
-    std::vector<const void*>& removed_symbols,
+    ArrayRef<JITCodeEntry*> jit_entries,
+    ArrayRef<const void*> removed_symbols,
     bool compress,
     /*out*/ size_t* num_symbols) {
   using ElfTypes = ElfRuntimeTypes;
   using Elf_Addr = typename ElfTypes::Addr;
   using Elf_Sym = typename ElfTypes::Sym;
+  const InstructionSet isa = kRuntimeISA;
   CHECK_EQ(sizeof(Elf_Addr), static_cast<size_t>(GetInstructionSetPointerSize(isa)));
   auto is_removed_symbol = [&removed_symbols](Elf_Addr addr) {
     const void* code_ptr = reinterpret_cast<const void*>(addr);
@@ -260,8 +260,8 @@
 
     using Reader = ElfDebugReader<ElfTypes>;
     std::deque<Reader> readers;
-    for (ArrayRef<const uint8_t> added_elf_file : added_elf_files) {
-      readers.emplace_back(added_elf_file);
+    for (JITCodeEntry* it : jit_entries) {
+      readers.emplace_back(GetJITCodeEntrySymFile(it));
     }
 
     // Write symbols names. All other data is buffered.
diff --git a/compiler/debug/elf_debug_writer.h b/compiler/debug/elf_debug_writer.h
index 32b2f67..ed43a0b 100644
--- a/compiler/debug/elf_debug_writer.h
+++ b/compiler/debug/elf_debug_writer.h
@@ -29,6 +29,7 @@
 
 namespace art {
 class OatHeader;
+struct JITCodeEntry;
 namespace mirror {
 class Class;
 }  // namespace mirror
@@ -56,10 +57,8 @@
     const MethodDebugInfo& method_info);
 
 std::vector<uint8_t> PackElfFileForJIT(
-    InstructionSet isa,
-    const InstructionSetFeatures* features,
-    std::vector<ArrayRef<const uint8_t>>& added_elf_files,
-    std::vector<const void*>& removed_symbols,
+    ArrayRef<JITCodeEntry*> jit_entries,
+    ArrayRef<const void*> removed_symbols,
     bool compress,
     /*out*/ size_t* num_symbols);
 
diff --git a/compiler/jit/jit_compiler.cc b/compiler/jit/jit_compiler.cc
index d84a132..f01554a 100644
--- a/compiler/jit/jit_compiler.cc
+++ b/compiler/jit/jit_compiler.cc
@@ -127,17 +127,14 @@
     REQUIRES_SHARED(Locks::mutator_lock_) {
   const CompilerOptions& compiler_options = GetCompilerOptions();
   if (compiler_options.GetGenerateDebugInfo()) {
+    InstructionSet isa = compiler_options.GetInstructionSet();
+    const InstructionSetFeatures* features = compiler_options.GetInstructionSetFeatures();
     const ArrayRef<mirror::Class*> types_array(types, count);
-    std::vector<uint8_t> elf_file = debug::WriteDebugElfFileForClasses(
-        kRuntimeISA, compiler_options.GetInstructionSetFeatures(), types_array);
-    // We never free debug info for types, so we don't need to provide a handle
-    // (which would have been otherwise used as identifier to remove it later).
-    AddNativeDebugInfoForJit(Thread::Current(),
-                             /*code_ptr=*/ nullptr,
-                             elf_file,
-                             /*pack*/ nullptr,
-                             compiler_options.GetInstructionSet(),
-                             compiler_options.GetInstructionSetFeatures());
+    std::vector<uint8_t> elf_file =
+        debug::WriteDebugElfFileForClasses(isa, features, types_array);
+
+    // NB: Don't allow packing since it would remove non-backtrace data.
+    AddNativeDebugInfoForJit(/*code_ptr=*/ nullptr, elf_file, /*allow_packing=*/ false);
   }
 }
 
@@ -145,6 +142,13 @@
   return GetCompilerOptions().GetGenerateDebugInfo();
 }
 
+std::vector<uint8_t> JitCompiler::PackElfFileForJIT(ArrayRef<JITCodeEntry*> elf_files,
+                                                    ArrayRef<const void*> removed_symbols,
+                                                    bool compress,
+                                                    /*out*/ size_t* num_symbols) {
+  return debug::PackElfFileForJIT(elf_files, removed_symbols, compress, num_symbols);
+}
+
 JitCompiler::JitCompiler() {
   compiler_options_.reset(new CompilerOptions());
   ParseCompilerOptions();
diff --git a/compiler/jit/jit_compiler.h b/compiler/jit/jit_compiler.h
index 737771f..c69a376 100644
--- a/compiler/jit/jit_compiler.h
+++ b/compiler/jit/jit_compiler.h
@@ -53,6 +53,11 @@
 
   void TypesLoaded(mirror::Class**, size_t count) REQUIRES_SHARED(Locks::mutator_lock_) override;
 
+  std::vector<uint8_t> PackElfFileForJIT(ArrayRef<JITCodeEntry*> elf_files,
+                                         ArrayRef<const void*> removed_symbols,
+                                         bool compress,
+                                         /*out*/ size_t* num_symbols) override;
+
  private:
   std::unique_ptr<CompilerOptions> compiler_options_;
   std::unique_ptr<Compiler> compiler_;
diff --git a/compiler/optimizing/optimizing_compiler.cc b/compiler/optimizing/optimizing_compiler.cc
index b1a3abe..5d06969 100644
--- a/compiler/optimizing/optimizing_compiler.cc
+++ b/compiler/optimizing/optimizing_compiler.cc
@@ -1481,17 +1481,13 @@
     const bool mini_debug_info = !compiler_options.GetGenerateDebugInfo();
 
     // Create entry for the single method that we just compiled.
-    std::vector<uint8_t> elf_file = debug::MakeElfFileForJIT(
-        compiler_options.GetInstructionSet(),
-        compiler_options.GetInstructionSetFeatures(),
-        mini_debug_info,
-        info);
-    AddNativeDebugInfoForJit(Thread::Current(),
-                             reinterpret_cast<const void*>(info.code_address),
-                             elf_file,
-                             mini_debug_info ? debug::PackElfFileForJIT : nullptr,
-                             compiler_options.GetInstructionSet(),
-                             compiler_options.GetInstructionSetFeatures());
+    InstructionSet isa = compiler_options.GetInstructionSet();
+    const InstructionSetFeatures* features = compiler_options.GetInstructionSetFeatures();
+    std::vector<uint8_t> elf = debug::MakeElfFileForJIT(isa, features, mini_debug_info, info);
+
+    // NB: Don't allow packing of full info since it would remove non-backtrace data.
+    const void* code_ptr = reinterpret_cast<const void*>(info.code_address);
+    AddNativeDebugInfoForJit(code_ptr, elf, /*allow_packing=*/ mini_debug_info);
   }
   Runtime::Current()->GetJit()->AddTimingLogger(logger);
 }
diff --git a/runtime/jit/debugger_interface.cc b/runtime/jit/debugger_interface.cc
index 2a5485c..2446a44 100644
--- a/runtime/jit/debugger_interface.cc
+++ b/runtime/jit/debugger_interface.cc
@@ -19,19 +19,19 @@
 #include <android-base/logging.h>
 
 #include "base/array_ref.h"
+#include "base/bit_utils.h"
 #include "base/logging.h"
 #include "base/mutex.h"
 #include "base/time_utils.h"
 #include "base/utils.h"
 #include "dex/dex_file.h"
+#include "jit/jit.h"
+#include "runtime.h"
 #include "thread-current-inl.h"
 #include "thread.h"
 
 #include <atomic>
 #include <cstddef>
-#include <deque>
-#include <map>
-#include <set>
 
 //
 // Debug interface for native tools (gdb, lldb, libunwind, simpleperf).
@@ -92,19 +92,32 @@
     JIT_UNREGISTER_FN
   };
 
-  struct JITCodeEntry {
+  // Public/stable binary interface.
+  struct JITCodeEntryPublic {
     // Atomic to ensure the reader can always iterate over the linked list
     // (e.g. the process could crash in the middle of writing this field).
     std::atomic<JITCodeEntry*> next_;
-    // Non-atomic. The reader should not use it. It is only used for deletion.
-    JITCodeEntry* prev_;
-    const uint8_t* symfile_addr_;
-    uint64_t symfile_size_;  // Beware of the offset (12 on x86; but 16 on ARM32).
+    JITCodeEntry* prev_;           // For linked list deletion.  Unused in readers.
+    const uint8_t* symfile_addr_;  // Address of the in-memory ELF file.
+    uint64_t symfile_size_;        // Beware of the offset (12 on x86; but 16 on ARM32).
 
     // Android-specific fields:
     uint64_t register_timestamp_;  // CLOCK_MONOTONIC time of entry registration.
   };
 
+  // Implementation-specific fields (which can be used only in this file).
+  struct JITCodeEntry : public JITCodeEntryPublic {
+    // Unpacked entries: Code address of the symbol in the ELF file.
+    // Packed entries: The start address of the covered memory range.
+    const void* addr_ = nullptr;
+    // Allow merging of ELF files to save space.
+    // Packing drops advanced DWARF data, so it is not always desirable.
+    bool allow_packing_ = false;
+    // Whether this entry has been LZMA compressed.
+    // Compression is expensive, so we don't always do it.
+    bool is_compressed_ = false;
+  };
+
   struct JITDescriptor {
     uint32_t version_ = 1;                      // NB: GDB supports only version 1.
     uint32_t action_flag_ = JIT_NOACTION;       // One of the JITAction enum values.
@@ -115,7 +128,7 @@
     uint8_t magic_[8] = {'A', 'n', 'd', 'r', 'o', 'i', 'd', '1'};
     uint32_t flags_ = 0;  // Reserved for future use. Must be 0.
     uint32_t sizeof_descriptor = sizeof(JITDescriptor);
-    uint32_t sizeof_entry = sizeof(JITCodeEntry);
+    uint32_t sizeof_entry = sizeof(JITCodeEntryPublic);
     std::atomic_uint32_t action_seqlock_{0};  // Incremented before and after any modification.
     uint64_t action_timestamp_ = 1;           // CLOCK_MONOTONIC time of last action.
   };
@@ -145,6 +158,10 @@
   JITDescriptor __dex_debug_descriptor GUARDED_BY(g_dex_debug_lock) {};
 }
 
+ArrayRef<const uint8_t> GetJITCodeEntrySymFile(JITCodeEntry* entry) {
+  return ArrayRef<const uint8_t>(entry->symfile_addr_, entry->symfile_size_);
+}
+
 // Mark the descriptor as "locked", so native tools know the data is being modified.
 static void ActionSeqlock(JITDescriptor& descriptor) {
   DCHECK_EQ(descriptor.action_seqlock_.load() & 1, 0u) << "Already locked";
@@ -165,7 +182,10 @@
     JITDescriptor& descriptor,
     void (*register_code_ptr)(),
     ArrayRef<const uint8_t> symfile,
-    bool copy_symfile) {
+    bool copy_symfile,
+    const void* addr = nullptr,
+    bool allow_packing = false,
+    bool is_compressed = false) {
   // Make a copy of the buffer to shrink it and to pass ownership to JITCodeEntry.
   if (copy_symfile) {
     uint8_t* copy = new uint8_t[symfile.size()];
@@ -179,13 +199,16 @@
   uint64_t timestamp = std::max(descriptor.action_timestamp_ + 1, NanoTime());
 
   JITCodeEntry* head = descriptor.head_.load(std::memory_order_relaxed);
-  JITCodeEntry* entry = new JITCodeEntry;
+  JITCodeEntry* entry = new JITCodeEntry();
   CHECK(entry != nullptr);
   entry->symfile_addr_ = symfile.data();
   entry->symfile_size_ = symfile.size();
   entry->prev_ = nullptr;
   entry->next_.store(head, std::memory_order_relaxed);
   entry->register_timestamp_ = timestamp;
+  entry->addr_ = addr;
+  entry->allow_packing_ = allow_packing;
+  entry->is_compressed_ = is_compressed;
 
   // We are going to modify the linked list, so take the seqlock.
   ActionSeqlock(descriptor);
@@ -244,196 +267,171 @@
   }
 }
 
-static std::map<const DexFile*, JITCodeEntry*> g_dex_debug_entries GUARDED_BY(g_dex_debug_lock);
-
 void AddNativeDebugInfoForDex(Thread* self, const DexFile* dexfile) {
   MutexLock mu(self, g_dex_debug_lock);
   DCHECK(dexfile != nullptr);
-  // This is just defensive check. The class linker should not register the dex file twice.
-  if (g_dex_debug_entries.count(dexfile) == 0) {
-    const ArrayRef<const uint8_t> symfile(dexfile->Begin(), dexfile->Size());
-    JITCodeEntry* entry = CreateJITCodeEntryInternal(__dex_debug_descriptor,
-                                                     __dex_debug_register_code_ptr,
-                                                     symfile,
-                                                     /*copy_symfile=*/ false);
-    g_dex_debug_entries.emplace(dexfile, entry);
-  }
+  const ArrayRef<const uint8_t> symfile(dexfile->Begin(), dexfile->Size());
+  CreateJITCodeEntryInternal(__dex_debug_descriptor,
+                             __dex_debug_register_code_ptr,
+                             symfile,
+                             /*copy_symfile=*/ false);
 }
 
 void RemoveNativeDebugInfoForDex(Thread* self, const DexFile* dexfile) {
   MutexLock mu(self, g_dex_debug_lock);
-  auto it = g_dex_debug_entries.find(dexfile);
+  DCHECK(dexfile != nullptr);
   // We register dex files in the class linker and free them in DexFile_closeDexFile, but
   // there might be cases where we load the dex file without using it in the class linker.
-  if (it != g_dex_debug_entries.end()) {
-    DeleteJITCodeEntryInternal(__dex_debug_descriptor,
-                               __dex_debug_register_code_ptr,
-                               /*entry=*/ it->second,
-                               /*free_symfile=*/ false);
-    g_dex_debug_entries.erase(it);
+  // On the other hand, single dex file might also be used with different class-loaders.
+  for (JITCodeEntry* entry = __dex_debug_descriptor.head_; entry != nullptr; ) {
+    JITCodeEntry* next = entry->next_;  // Save next pointer before we free the memory.
+    if (entry->symfile_addr_ == dexfile->Begin()) {
+      DeleteJITCodeEntryInternal(__dex_debug_descriptor,
+                                 __dex_debug_register_code_ptr,
+                                 entry,
+                                 /*free_symfile=*/ false);
+    }
+    entry = next;
   }
 }
 
-// Mapping from handle to entry. Used to manage life-time of the entries.
-using JITCodeEntries = std::multimap<const void*, JITCodeEntry*>;
-static JITCodeEntries g_uncompressed_jit_debug_entries GUARDED_BY(g_jit_debug_lock);
-static JITCodeEntries g_compressed_jit_debug_entries GUARDED_BY(g_jit_debug_lock);
+// Size of JIT code range covered by each packed JITCodeEntry.
+static constexpr uint32_t kJitRepackGroupSize = 64 * KB;
 
-// Number of entries added since last packing.  Used to pack entries in bulk.
-static size_t g_jit_num_unpacked_entries GUARDED_BY(g_jit_debug_lock) = 0;
-static constexpr uint32_t kJitMaxUnpackedEntries = 64;
+// Automatically call the repack method every 'n' new entries.
+static constexpr uint32_t kJitRepackFrequency = 64;
+static uint32_t g_jit_num_unpacked_entries = 0;
 
-// We postpone removal so that it is done in bulk.
-static std::set<const void*> g_jit_removed_entries GUARDED_BY(g_jit_debug_lock);
-
-// Split the JIT code cache into groups of fixed size and create singe JITCodeEntry for each group.
+// Split the JIT code cache into groups of fixed size and create single JITCodeEntry for each group.
 // The start address of method's code determines which group it belongs to.  The end is irrelevant.
-// As a consequnce, newly added mini debug infos will be merged and old ones (GCed) will be pruned.
-static void RepackEntries(PackElfFileForJITFunction pack,
-                          InstructionSet isa,
-                          const InstructionSetFeatures* features,
-                          bool compress,
-                          /*inout*/ JITCodeEntries* entries)
+// New mini debug infos will be merged if possible, and entries for GCed functions will be removed.
+static void RepackEntries(bool compress, ArrayRef<const void*> removed)
     REQUIRES(g_jit_debug_lock) {
-  // Size of memory range covered by each JITCodeEntry.
-  // The number of methods per entry is variable (depending on how many fit in that range).
-  constexpr uint32_t kGroupSize = 64 * KB;
+  DCHECK(std::is_sorted(removed.begin(), removed.end()));
+  jit::Jit* jit = Runtime::Current()->GetJit();
+  if (jit == nullptr) {
+    return;
+  }
 
-  CHECK(pack != nullptr);
-  JITCodeEntries packed_entries;
-  std::vector<ArrayRef<const uint8_t>> added;
-  std::vector<const void*> removed;
-  while (!entries->empty()) {
-    const void* group_ptr = AlignDown(entries->begin()->first, kGroupSize);
-    const void* group_end = reinterpret_cast<const uint8_t*>(group_ptr) + kGroupSize;
+  // Collect entries that we want to pack.
+  std::vector<JITCodeEntry*> entries;
+  entries.reserve(2 * kJitRepackFrequency);
+  for (JITCodeEntry* it = __jit_debug_descriptor.head_; it != nullptr; it = it->next_) {
+    if (it->allow_packing_) {
+      if (!compress && it->is_compressed_ && removed.empty()) {
+        continue;  // If we are not compressing, also avoid decompressing.
+      }
+      entries.push_back(it);
+    }
+  }
+  auto cmp = [](JITCodeEntry* lhs, JITCodeEntry* rhs) { return lhs->addr_ < rhs->addr_; };
+  std::sort(entries.begin(), entries.end(), cmp);  // Sort by address.
 
-    // Collect all entries that have been added or removed within our memory range.
-    added.clear();
-    auto add_it = entries->begin();
-    for (; add_it != entries->end() && add_it->first < group_end; ++add_it) {
-      JITCodeEntry* entry = add_it->second;
-      added.emplace_back(entry->symfile_addr_, entry->symfile_size_);
-    }
-    removed.clear();
-    auto remove_it = g_jit_removed_entries.lower_bound(group_ptr);
-    for (; remove_it != g_jit_removed_entries.end() && *remove_it < group_end; ++remove_it) {
-      removed.push_back(*remove_it);
-    }
-    CHECK_GT(added.size(), 0u);
-    if (added.size() == 1 && removed.size() == 0) {
-      packed_entries.insert(entries->extract(entries->begin()));
-      continue;  // Nothing changed in this memory range.
+  // Process the entries in groups (each spanning memory range of size kJitRepackGroupSize).
+  for (auto group_it = entries.begin(); group_it != entries.end();) {
+    const void* group_ptr = AlignDown((*group_it)->addr_, kJitRepackGroupSize);
+    const void* group_end = reinterpret_cast<const uint8_t*>(group_ptr) + kJitRepackGroupSize;
+
+    // Find all entries in this group (each entry is an in-memory ELF file).
+    auto begin = group_it;
+    auto end = std::find_if(begin, entries.end(), [=](auto* e) { return e->addr_ >= group_end; });
+    CHECK(end > begin);
+    ArrayRef<JITCodeEntry*> elfs(&*begin, end - begin);
+
+    // Find all symbols that have been removed in this memory range.
+    auto removed_begin = std::lower_bound(removed.begin(), removed.end(), group_ptr);
+    auto removed_end = std::lower_bound(removed.begin(), removed.end(), group_end);
+    CHECK(removed_end >= removed_begin);
+    ArrayRef<const void*> removed_subset(&*removed_begin, removed_end - removed_begin);
+
+    // Bail out early if there is nothing to do for this group.
+    if (elfs.size() == 1 && removed_subset.empty() && (*begin)->is_compressed_ == compress) {
+      group_it = end;  // Go to next group.
+      continue;
     }
 
     // Create new single JITCodeEntry that covers this memory range.
     uint64_t start_time = MicroTime();
-    size_t symbols;
-    std::vector<uint8_t> packed = pack(isa, features, added, removed, compress, &symbols);
+    size_t live_symbols;
+    std::vector<uint8_t> packed = jit->GetJitCompiler()->PackElfFileForJIT(
+        elfs, removed_subset, compress, &live_symbols);
     VLOG(jit)
         << "JIT mini-debug-info repacked"
         << " for " << group_ptr
         << " in " << MicroTime() - start_time << "us"
-        << " files=" << added.size()
-        << " removed=" << removed.size()
-        << " symbols=" << symbols
-        << " size=" << PrettySize(packed.size())
-        << " compress=" << compress;
+        << " elfs=" << elfs.size()
+        << " dead=" << removed_subset.size()
+        << " live=" << live_symbols
+        << " size=" << packed.size() << (compress ? "(lzma)" : "");
 
     // Replace the old entries with the new one (with their lifetime temporally overlapping).
-    packed_entries.emplace(group_ptr, CreateJITCodeEntryInternal(
+    CreateJITCodeEntryInternal(
         __jit_debug_descriptor,
         __jit_debug_register_code_ptr,
         ArrayRef<const uint8_t>(packed),
-        /*copy_symfile=*/ true));
-    for (auto it = entries->begin(); it != add_it; it = entries->erase(it)) {
+        /*copy_symfile=*/ true,
+        /*addr_=*/ group_ptr,
+        /*allow_packing_=*/ true,
+        /*is_compressed_=*/ compress);
+    for (auto it : elfs) {
       DeleteJITCodeEntryInternal(__jit_debug_descriptor,
                                  __jit_debug_register_code_ptr,
-                                 /*entry=*/ it->second,
+                                 /*entry=*/ it,
                                  /*free_symfile=*/ true);
     }
+    group_it = end;  // Go to next group.
   }
-  entries->swap(packed_entries);
+  g_jit_num_unpacked_entries = 0;
 }
 
-void AddNativeDebugInfoForJit(Thread* self,
-                              const void* code_ptr,
+void AddNativeDebugInfoForJit(const void* code_ptr,
                               const std::vector<uint8_t>& symfile,
-                              PackElfFileForJITFunction pack,
-                              InstructionSet isa,
-                              const InstructionSetFeatures* features) {
-  MutexLock mu(self, g_jit_debug_lock);
+                              bool allow_packing) {
+  MutexLock mu(Thread::Current(), g_jit_debug_lock);
   DCHECK_NE(symfile.size(), 0u);
 
-  // Pack and compress all entries. This will run on first compilation after a GC.
-  // Must be done before addition in case the added code_ptr is in the removed set.
-  if (!g_jit_removed_entries.empty()) {
-    g_compressed_jit_debug_entries.merge(g_uncompressed_jit_debug_entries);
-    if (pack != nullptr) {
-      RepackEntries(pack, isa, features, /*compress=*/ true, &g_compressed_jit_debug_entries);
-    } else {
-      // If repacking function is not provided, just remove the individual entries.
-      for (const void* removed_code_ptr : g_jit_removed_entries) {
-        auto it = g_compressed_jit_debug_entries.find(removed_code_ptr);
-        if (it != g_compressed_jit_debug_entries.end()) {
-          DeleteJITCodeEntryInternal(__jit_debug_descriptor,
-                                     __jit_debug_register_code_ptr,
-                                     /*entry=*/ it->second,
-                                     /*free_symfile=*/ true);
-          g_compressed_jit_debug_entries.erase(it);
-        }
-      }
-    }
-    g_jit_removed_entries.clear();
-    g_jit_num_unpacked_entries = 0;
-  }
-
-  JITCodeEntry* entry = CreateJITCodeEntryInternal(
+  CreateJITCodeEntryInternal(
       __jit_debug_descriptor,
       __jit_debug_register_code_ptr,
       ArrayRef<const uint8_t>(symfile),
-      /*copy_symfile=*/ true);
+      /*copy_symfile=*/ true,
+      /*addr=*/ code_ptr,
+      /*allow_packing=*/ allow_packing,
+      /*is_compressed=*/ false);
 
   VLOG(jit)
       << "JIT mini-debug-info added"
       << " for " << code_ptr
       << " size=" << PrettySize(symfile.size());
 
-  // We don't provide code_ptr for type debug info, which means we cannot free it later.
-  // (this only happens when --generate-debug-info flag is enabled for the purpose
-  // of being debugged with gdb; it does not happen for debuggable apps by default).
-  if (code_ptr == nullptr) {
-    return;
-  }
-
-  g_uncompressed_jit_debug_entries.emplace(code_ptr, entry);
-  // Count how many entries we have added since the last mini-debug-info packing.
-  // We avoid g_uncompressed_jit_debug_entries.size() because it can shrink during packing.
-  ++g_jit_num_unpacked_entries;
-
+  // Automatically repack entries on regular basis to save space.
   // Pack (but don't compress) recent entries - this is cheap and reduces memory use by ~4x.
   // We delay compression until after GC since it is more expensive (and saves further ~4x).
-  if (g_jit_num_unpacked_entries >= kJitMaxUnpackedEntries && pack != nullptr) {
-    RepackEntries(pack, isa, features, /*compress=*/ false, &g_uncompressed_jit_debug_entries);
-    g_jit_num_unpacked_entries = 0;
+  if (++g_jit_num_unpacked_entries >= kJitRepackFrequency) {
+    RepackEntries(/*compress=*/ false, /*removed=*/ ArrayRef<const void*>());
   }
 }
 
-void RemoveNativeDebugInfoForJit(Thread* self, const void* code_ptr) {
-  MutexLock mu(self, g_jit_debug_lock);
-  // We generate JIT native debug info only if the right runtime flags are enabled,
-  // but we try to remove it unconditionally whenever code is freed from JIT cache.
-  if (!g_uncompressed_jit_debug_entries.empty() || !g_compressed_jit_debug_entries.empty()) {
-    g_jit_removed_entries.insert(code_ptr);
+void RemoveNativeDebugInfoForJit(ArrayRef<const void*> removed) {
+  MutexLock mu(Thread::Current(), g_jit_debug_lock);
+  RepackEntries(/*compress=*/ true, removed);
+
+  // Remove entries which are not allowed to be packed (containing single method each).
+  for (JITCodeEntry* it = __jit_debug_descriptor.head_; it != nullptr; it = it->next_) {
+    if (!it->allow_packing_ && std::binary_search(removed.begin(), removed.end(), it->addr_)) {
+      DeleteJITCodeEntryInternal(__jit_debug_descriptor,
+                                 __jit_debug_register_code_ptr,
+                                 /*entry=*/ it,
+                                 /*free_symfile=*/ true);
+    }
   }
 }
 
 size_t GetJitMiniDebugInfoMemUsage() {
   MutexLock mu(Thread::Current(), g_jit_debug_lock);
   size_t size = 0;
-  for (const auto& entries : {g_uncompressed_jit_debug_entries, g_compressed_jit_debug_entries}) {
-    for (const auto& entry : entries) {
-      size += sizeof(JITCodeEntry) + entry.second->symfile_size_ + /*map entry*/ 4 * sizeof(void*);
-    }
+  for (JITCodeEntry* it = __jit_debug_descriptor.head_; it != nullptr; it = it->next_) {
+    size += sizeof(JITCodeEntry) + it->symfile_size_;
   }
   return size;
 }
diff --git a/runtime/jit/debugger_interface.h b/runtime/jit/debugger_interface.h
index f84f08d..be5e5ac 100644
--- a/runtime/jit/debugger_interface.h
+++ b/runtime/jit/debugger_interface.h
@@ -17,6 +17,7 @@
 #ifndef ART_RUNTIME_JIT_DEBUGGER_INTERFACE_H_
 #define ART_RUNTIME_JIT_DEBUGGER_INTERFACE_H_
 
+#include <functional>
 #include <inttypes.h>
 #include <vector>
 
@@ -29,16 +30,9 @@
 class DexFile;
 class Mutex;
 class Thread;
+struct JITCodeEntry;
 
-// This method is declared in the compiler library.
-// We need to pass it by pointer to be able to call it from runtime.
-typedef std::vector<uint8_t> PackElfFileForJITFunction(
-    InstructionSet isa,
-    const InstructionSetFeatures* features,
-    std::vector<ArrayRef<const uint8_t>>& added_elf_files,
-    std::vector<const void*>& removed_symbols,
-    bool compress,
-    /*out*/ size_t* num_symbols);
+ArrayRef<const uint8_t> GetJITCodeEntrySymFile(JITCodeEntry*);
 
 // Notify native tools (e.g. libunwind) that DEX file has been opened.
 void AddNativeDebugInfoForDex(Thread* self, const DexFile* dexfile);
@@ -46,19 +40,16 @@
 // Notify native tools (e.g. libunwind) that DEX file has been closed.
 void RemoveNativeDebugInfoForDex(Thread* self, const DexFile* dexfile);
 
-// Notify native tools (e.g. libunwind) that JIT has compiled a new method.
+// Notify native tools (e.g. libunwind) that JIT has compiled a single new method.
 // The method will make copy of the passed ELF file (to shrink it to the minimum size).
-// If packing function is provided, ELF files can be merged to save space
-// (however, the merging drops advanced gdb debug-info as it is too complex).
-void AddNativeDebugInfoForJit(Thread* self,
-                              const void* code_ptr,
+// If packing is allowed, the ELF file might be merged with others to save space
+// (however, this drops all ELF sections other than symbols names and unwinding info).
+void AddNativeDebugInfoForJit(const void* code_ptr,
                               const std::vector<uint8_t>& symfile,
-                              PackElfFileForJITFunction pack,
-                              InstructionSet isa,
-                              const InstructionSetFeatures* features);
+                              bool allow_packing);
 
 // Notify native tools (e.g. libunwind) that JIT code has been garbage collected.
-void RemoveNativeDebugInfoForJit(Thread* self, const void* code_ptr);
+void RemoveNativeDebugInfoForJit(ArrayRef<const void*> removed_code_ptrs);
 
 // Returns approximate memory used by debug info for JIT code.
 size_t GetJitMiniDebugInfoMemUsage();
diff --git a/runtime/jit/jit.h b/runtime/jit/jit.h
index c264f7c..3a103f5 100644
--- a/runtime/jit/jit.h
+++ b/runtime/jit/jit.h
@@ -23,6 +23,7 @@
 #include "base/runtime_debug.h"
 #include "base/timing_logger.h"
 #include "handle.h"
+#include "jit/debugger_interface.h"
 #include "jit/profile_saver_options.h"
 #include "obj_ptr.h"
 #include "thread_pool.h"
@@ -175,6 +176,11 @@
       REQUIRES_SHARED(Locks::mutator_lock_) = 0;
   virtual bool GenerateDebugInfo() = 0;
   virtual void ParseCompilerOptions() = 0;
+
+  virtual std::vector<uint8_t> PackElfFileForJIT(ArrayRef<JITCodeEntry*> elf_files,
+                                                 ArrayRef<const void*> removed_symbols,
+                                                 bool compress,
+                                                 /*out*/ size_t* num_symbols) = 0;
 };
 
 class Jit {
@@ -202,6 +208,10 @@
     return code_cache_;
   }
 
+  JitCompilerInterface* GetJitCompiler() const {
+    return jit_compiler_;
+  }
+
   void CreateThreadPool();
   void DeleteThreadPool();
   void WaitForWorkersToBeCreated();
diff --git a/runtime/jit/jit_code_cache.cc b/runtime/jit/jit_code_cache.cc
index b5c3279..dc2bb7c 100644
--- a/runtime/jit/jit_code_cache.cc
+++ b/runtime/jit/jit_code_cache.cc
@@ -490,15 +490,17 @@
   }
 }
 
-void JitCodeCache::FreeCodeAndData(const void* code_ptr) {
+void JitCodeCache::FreeCodeAndData(const void* code_ptr, bool free_debug_info) {
   if (IsInZygoteExecSpace(code_ptr)) {
     // No need to free, this is shared memory.
     return;
   }
   uintptr_t allocation = FromCodeToAllocation(code_ptr);
-  // Notify native debugger that we are about to remove the code.
-  // It does nothing if we are not using native debugger.
-  RemoveNativeDebugInfoForJit(Thread::Current(), code_ptr);
+  if (free_debug_info) {
+    // Remove compressed mini-debug info for the method.
+    // TODO: This is expensive, so we should always do it in the caller in bulk.
+    RemoveNativeDebugInfoForJit(ArrayRef<const void*>(&code_ptr, 1));
+  }
   if (OatQuickMethodHeader::FromCodePointer(code_ptr)->IsOptimized()) {
     private_region_.FreeData(GetRootTable(code_ptr));
   }  // else this is a JNI stub without any data.
@@ -519,9 +521,18 @@
         ->RemoveDependentsWithMethodHeaders(method_headers);
   }
 
+  // Remove compressed mini-debug info for the methods.
+  std::vector<const void*> removed_symbols;
+  removed_symbols.reserve(method_headers.size());
+  for (const OatQuickMethodHeader* method_header : method_headers) {
+    removed_symbols.push_back(method_header->GetCode());
+  }
+  std::sort(removed_symbols.begin(), removed_symbols.end());
+  RemoveNativeDebugInfoForJit(ArrayRef<const void*>(removed_symbols));
+
   ScopedCodeCacheWrite scc(private_region_);
   for (const OatQuickMethodHeader* method_header : method_headers) {
-    FreeCodeAndData(method_header->GetCode());
+    FreeCodeAndData(method_header->GetCode(), /*free_debug_info=*/ false);
   }
 }
 
diff --git a/runtime/jit/jit_code_cache.h b/runtime/jit/jit_code_cache.h
index 284a36a..6aa5f31 100644
--- a/runtime/jit/jit_code_cache.h
+++ b/runtime/jit/jit_code_cache.h
@@ -388,7 +388,8 @@
       REQUIRES(Locks::mutator_lock_);
 
   // Free code and data allocations for `code_ptr`.
-  void FreeCodeAndData(const void* code_ptr) REQUIRES(Locks::jit_lock_);
+  void FreeCodeAndData(const void* code_ptr, bool free_debug_info = true)
+      REQUIRES(Locks::jit_lock_);
 
   // Number of bytes allocated in the code cache.
   size_t CodeCacheSize() REQUIRES(!Locks::jit_lock_);