tp: improve plugin interface (#5368)

Learnings from trying to port various users to use the plugin API.
diff --git a/src/trace_processor/core/plugin/BUILD.gn b/src/trace_processor/core/plugin/BUILD.gn
index 6c7d24c..0f35661 100644
--- a/src/trace_processor/core/plugin/BUILD.gn
+++ b/src/trace_processor/core/plugin/BUILD.gn
@@ -22,5 +22,6 @@
     "../../../base",
     "../../containers",
     "../../types",
+    "../util",
   ]
 }
diff --git a/src/trace_processor/core/plugin/plugin.cc b/src/trace_processor/core/plugin/plugin.cc
index fbb7af5..39d8a88 100644
--- a/src/trace_processor/core/plugin/plugin.cc
+++ b/src/trace_processor/core/plugin/plugin.cc
@@ -17,8 +17,9 @@
 #include "src/trace_processor/core/plugin/plugin.h"
 
 #include <cstddef>
+#include <cstdint>
+#include <limits>
 #include <memory>
-#include <string>
 #include <utility>
 #include <vector>
 
@@ -28,61 +29,21 @@
 
 namespace perfetto::trace_processor {
 
-// Global linked list head for plugin registrations.
-static PluginRegistration* g_plugin_head = nullptr;
-
-PluginRegistration::PluginRegistration(Factory f,
-                                       const void* id,
-                                       const void* const* deps,
-                                       size_t n_deps)
-    : next(g_plugin_head),
-      factory(f),
-      plugin_id(id),
-      dep_ids(deps),
-      dep_count(n_deps) {
-  g_plugin_head = this;
-}
-
-PluginRegistration* GetPluginRegistrations() {
-  return g_plugin_head;
-}
-
-// Default no-op implementations.
-PluginBase::~PluginBase() = default;
-std::unique_ptr<Destructible> PluginBase::CreateStorage(
-    TraceProcessorContext*) {
-  return nullptr;
-}
-void PluginBase::RegisterImporters(TraceProcessorContext*, Destructible*) {}
-void PluginBase::RegisterDataframes(TraceProcessorContext*,
-                                    Destructible*,
-                                    std::vector<PluginDataframe>&) {}
-void PluginBase::RegisterStaticTableFunctions(
-    TraceProcessorContext*,
-    Destructible*,
-    std::vector<std::unique_ptr<StaticTableFunction>>&) {}
-void PluginBase::RegisterSqliteModules(TraceProcessorContext*,
-                                       Destructible*,
-                                       std::vector<SqliteModuleRegistration>&) {
-}
-void PluginBase::RegisterSqlModules(
-    TraceProcessorContext*,
-    Destructible*,
-    std::vector<std::pair<std::string, std::string>>&) {}
-
 namespace {
 
-// Returns the topologically sorted order of plugin registrations.
-// The result is cached in a static local since the global linked list
-// is fixed after static initialization.
-std::vector<PluginRegistration*> TopologicalSort() {
+// Global linked list head for plugin registrations.
+PluginRegistration* g_plugin_head = nullptr;
+
+// Topologically sorts registrations and pre-resolves dependency indices.
+// Called once and cached.
+PluginSet BuildPluginSet() {
   // Collect all registrations.
   std::vector<PluginRegistration*> regs;
-  for (auto* r = GetPluginRegistrations(); r; r = r->next) {
+  for (auto* r = g_plugin_head; r; r = r->next) {
     regs.push_back(r);
   }
 
-  // Build a map from plugin_id -> index for dependency resolution.
+  // Build a map from plugin_id -> index in |regs|.
   base::FlatHashMap<const void*, size_t> id_to_idx;
   for (size_t i = 0; i < regs.size(); ++i) {
     id_to_idx[regs[i]->plugin_id] = i;
@@ -98,8 +59,6 @@
   // Kahn's algorithm for topological sort.
   size_t n = regs.size();
   std::vector<size_t> in_degree(n, 0);
-
-  // Build adjacency: for each plugin, which plugins depend on it.
   std::vector<std::vector<size_t>> dependents(n);
   for (size_t i = 0; i < n; ++i) {
     for (size_t d = 0; d < regs[i]->dep_count; ++d) {
@@ -109,7 +68,6 @@
     }
   }
 
-  // Start with plugins that have no dependencies.
   std::vector<size_t> queue;
   for (size_t i = 0; i < n; ++i) {
     if (in_degree[i] == 0) {
@@ -129,34 +87,64 @@
       }
     }
   }
-
-  // If we didn't process all plugins, there's a cycle.
   PERFETTO_CHECK(sorted_order.size() == n);
 
-  std::vector<PluginRegistration*> result;
-  result.reserve(n);
-  for (size_t idx : sorted_order) {
-    result.push_back(regs[idx]);
+  // Build a map from original reg index -> position in sorted output,
+  // so we can resolve dep indices into the final array.
+  base::FlatHashMap<const void*, size_t> id_to_sorted_idx;
+  for (size_t i = 0; i < sorted_order.size(); ++i) {
+    id_to_sorted_idx[regs[sorted_order[i]]->plugin_id] = i;
+  }
+
+  // Build PluginSet with pre-resolved dep indices.
+  PluginSet result;
+  result.entries.reserve(n);
+  for (size_t orig_idx : sorted_order) {
+    PluginSet::Entry entry;
+    entry.factory = regs[orig_idx]->factory;
+    for (size_t d = 0; d < regs[orig_idx]->dep_count; ++d) {
+      size_t* dep_sorted = id_to_sorted_idx.Find(regs[orig_idx]->dep_ids[d]);
+      PERFETTO_CHECK(dep_sorted != nullptr);
+      entry.dep_indices.push_back(*dep_sorted);
+    }
+    result.entries.push_back(std::move(entry));
   }
   return result;
 }
 
-const std::vector<PluginRegistration*>& GetSortedPluginRegistrations() {
-  static base::NoDestructor<std::vector<PluginRegistration*>> sorted(
-      TopologicalSort());
-  return sorted.ref();
-}
-
 }  // namespace
 
-std::vector<std::unique_ptr<PluginBase>> CreatePlugins() {
-  const auto& sorted = GetSortedPluginRegistrations();
-  std::vector<std::unique_ptr<PluginBase>> plugins;
-  plugins.reserve(sorted.size());
-  for (auto* reg : sorted) {
-    plugins.push_back(reg->factory());
-  }
-  return plugins;
+PluginRegistration::PluginRegistration(Factory f,
+                                       const void* id,
+                                       const void* const* deps,
+                                       size_t n_deps)
+    : next(g_plugin_head),
+      factory(f),
+      plugin_id(id),
+      dep_ids(deps),
+      dep_count(n_deps) {
+  g_plugin_head = this;
+}
+
+// Default no-op implementations.
+PluginBase::~PluginBase() = default;
+void PluginBase::RegisterImporters(TraceReaderRegistry&) {}
+void PluginBase::RegisterProtoImporterModules(ProtoImporterModuleContext*) {}
+void PluginBase::RegisterDataframes(std::vector<PluginDataframe>&) {}
+void PluginBase::RegisterStaticTableFunctions(
+    std::vector<std::unique_ptr<StaticTableFunction>>&) {}
+void PluginBase::RegisterSqliteModules(std::vector<SqliteModuleRegistration>&) {
+}
+uint64_t PluginBase::GetBoundsMutationCount() {
+  return 0;
+}
+std::pair<int64_t, int64_t> PluginBase::GetTimestampBounds() {
+  return {std::numeric_limits<int64_t>::max(), 0};
+}
+
+const PluginSet& GetPluginSet() {
+  static base::NoDestructor<PluginSet> set(BuildPluginSet());
+  return set.ref();
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/core/plugin/plugin.h b/src/trace_processor/core/plugin/plugin.h
index 7079f2d..114d91e 100644
--- a/src/trace_processor/core/plugin/plugin.h
+++ b/src/trace_processor/core/plugin/plugin.h
@@ -19,13 +19,14 @@
 
 #include <array>
 #include <cstddef>
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <type_traits>
 #include <utility>
 #include <vector>
 
-#include "src/trace_processor/types/destructible.h"
+#include "src/trace_processor/core/util/type_set.h"
 
 struct sqlite3_module;
 
@@ -37,7 +38,9 @@
 
 class StaticTableFunction;
 class TraceProcessorContext;
+class TraceReaderRegistry;
 class TraceStorage;
+struct ProtoImporterModuleContext;
 
 // Lightweight struct for plugin dataframe registration.
 struct PluginDataframe {
@@ -94,173 +97,73 @@
 // runtime ID via the address of a per-instantiation static member.
 template <typename T>
 struct PluginTag {
-  static inline constexpr char kTag = 0;
+  static constexpr char kTag = 0;
   static constexpr const void* Id() { return &kTag; }
 };
 
-// Non-templated base class for type-erased storage and virtual dispatch.
+// Non-templated base class for plugins.
 //
-// Plugins must NOT store mutable state on the plugin object itself. State
-// should either live in the Storage object (for data that persists beyond
-// parsing) or inside the parser/importer that the plugin creates.
+// The plugin IS the context: it carries its resolved dependency pointers and
+// a back-pointer to TraceProcessorContext. The framework sets these fields
+// after construction; lifecycle methods access them via `this`.
 //
-// TraceProcessorImpl calls these methods, passing both the context and the
-// type-erased storage pointer. Subclasses should not override these directly;
-// instead, override the typed methods on Plugin<>.
+// How the plugin organizes its internal state is an implementation detail.
+// Subclasses should not override the PluginBase virtuals directly; instead,
+// inherit from Plugin<> which provides identity and typed dep access.
 class PluginBase {
  public:
   virtual ~PluginBase();
 
-  // Creates the plugin's opaque storage. Called once by TraceProcessorImpl;
-  // the returned pointer is owned by TraceStorage and passed back into
-  // all subsequent lifecycle methods.
-  virtual std::unique_ptr<Destructible> CreateStorage(
-      TraceProcessorContext* context);
+  // Identity and dependency accessors for the framework.
+  virtual const void* GetPluginId() const = 0;
+  virtual const void* const* GetDepIds() const = 0;
+  virtual size_t GetDepCount() const = 0;
 
-  virtual void RegisterImporters(TraceProcessorContext* context,
-                                 Destructible* storage);
-  virtual void RegisterDataframes(TraceProcessorContext* context,
-                                  Destructible* storage,
-                                  std::vector<PluginDataframe>& tables);
+  virtual void RegisterImporters(TraceReaderRegistry& registry);
+  virtual void RegisterProtoImporterModules(
+      ProtoImporterModuleContext* module_context);
+  virtual void RegisterDataframes(std::vector<PluginDataframe>& tables);
   virtual void RegisterStaticTableFunctions(
-      TraceProcessorContext* context,
-      Destructible* storage,
       std::vector<std::unique_ptr<StaticTableFunction>>& fns);
   virtual void RegisterSqliteModules(
-      TraceProcessorContext* context,
-      Destructible* storage,
       std::vector<SqliteModuleRegistration>& modules);
-  // Allows plugins to contribute SQL modules to the stdlib. Each entry is a
-  // pair of (module_key, sql_content) where module_key follows the stdlib
-  // naming convention (e.g., "etm.decode").
-  virtual void RegisterSqlModules(
-      TraceProcessorContext* context,
-      Destructible* storage,
-      std::vector<std::pair<std::string, std::string>>& modules);
+
+  virtual uint64_t GetBoundsMutationCount();
+  virtual std::pair<int64_t, int64_t> GetTimestampBounds();
+
+  // --- Framework-managed fields. Set by TraceProcessorImpl after creation. ---
+  TraceProcessorContext* trace_context_ = nullptr;
+  std::vector<PluginBase*> resolved_deps_;
 };
 
-// Templated subclass that provides identity, dependency tracking, and typed
-// storage dispatch.
+// Templated subclass that provides identity, dependency tracking, typed
+// dependency access, and typed factory wrappers for lifecycle methods.
 //
 // Self: CRTP parameter (the concrete plugin class).
-// Storage: plugin-owned state type, must inherit from Destructible.
-//          Use void (or omit) if no storage is needed.
-// Deps: prerequisite plugin classes.
+// Deps: prerequisite plugin classes. Declares the dependency graph for
+//       topological sorting. Access deps at runtime via dependency<T>().
 //
 // The template parameters serve three purposes:
 // 1. Compile-time: forces #include of dep headers -> forces GN dep
 // 2. Link-time: if dep's .cc isn't compiled, GN dep missing -> build fails
 // 3. Runtime: topological sort verifies all deps are registered
-template <typename Self, typename Storage = void, typename... Deps>
+template <typename Self, typename... Deps>
 class Plugin : public PluginBase {
  public:
   static constexpr const void* kPluginId = PluginTag<Self>::Id();
   static constexpr std::array<const void*, sizeof...(Deps)> kDepIds = {
       PluginTag<Deps>::Id()...};
 
-  // Override these in the concrete plugin class.
-  virtual std::unique_ptr<Storage> CreatePluginStorage(TraceProcessorContext*) {
-    return nullptr;
-  }
-  virtual void RegisterImporters(TraceProcessorContext*, Storage*) {}
-  virtual void RegisterDataframes(TraceProcessorContext*,
-                                  Storage*,
-                                  std::vector<PluginDataframe>&) {}
-  virtual void RegisterStaticTableFunctions(
-      TraceProcessorContext*,
-      Storage*,
-      std::vector<std::unique_ptr<StaticTableFunction>>&) {}
-  virtual void RegisterSqliteModules(TraceProcessorContext*,
-                                     Storage*,
-                                     std::vector<SqliteModuleRegistration>&) {}
-  virtual void RegisterSqlModules(
-      TraceProcessorContext*,
-      Storage*,
-      std::vector<std::pair<std::string, std::string>>&) {}
+  const void* GetPluginId() const final { return kPluginId; }
+  const void* const* GetDepIds() const final { return kDepIds.data(); }
+  size_t GetDepCount() const final { return kDepIds.size(); }
 
- private:
-  std::unique_ptr<Destructible> CreateStorage(
-      TraceProcessorContext* ctx) final {
-    return static_cast<Self*>(this)->CreatePluginStorage(ctx);
-  }
-  void RegisterImporters(TraceProcessorContext* ctx, Destructible* s) final {
-    static_cast<Self*>(this)->RegisterImporters(ctx, static_cast<Storage*>(s));
-  }
-  void RegisterDataframes(TraceProcessorContext* ctx,
-                          Destructible* s,
-                          std::vector<PluginDataframe>& tables) final {
-    static_cast<Self*>(this)->RegisterDataframes(ctx, static_cast<Storage*>(s),
-                                                 tables);
-  }
-  void RegisterStaticTableFunctions(
-      TraceProcessorContext* ctx,
-      Destructible* s,
-      std::vector<std::unique_ptr<StaticTableFunction>>& fns) final {
-    static_cast<Self*>(this)->RegisterStaticTableFunctions(
-        ctx, static_cast<Storage*>(s), fns);
-  }
-  void RegisterSqliteModules(
-      TraceProcessorContext* ctx,
-      Destructible* s,
-      std::vector<SqliteModuleRegistration>& modules) final {
-    static_cast<Self*>(this)->RegisterSqliteModules(
-        ctx, static_cast<Storage*>(s), modules);
-  }
-  void RegisterSqlModules(
-      TraceProcessorContext* ctx,
-      Destructible* s,
-      std::vector<std::pair<std::string, std::string>>& modules) final {
-    static_cast<Self*>(this)->RegisterSqlModules(ctx, static_cast<Storage*>(s),
-                                                 modules);
-  }
-};
-
-// Specialization for plugins with no storage.
-template <typename Self, typename... Deps>
-class Plugin<Self, void, Deps...> : public PluginBase {
- public:
-  static constexpr const void* kPluginId = PluginTag<Self>::Id();
-  static constexpr std::array<const void*, sizeof...(Deps)> kDepIds = {
-      PluginTag<Deps>::Id()...};
-
-  virtual void RegisterImporters(TraceProcessorContext*) {}
-  virtual void RegisterDataframes(TraceProcessorContext*,
-                                  std::vector<PluginDataframe>&) {}
-  virtual void RegisterStaticTableFunctions(
-      TraceProcessorContext*,
-      std::vector<std::unique_ptr<StaticTableFunction>>&) {}
-  virtual void RegisterSqliteModules(TraceProcessorContext*,
-                                     std::vector<SqliteModuleRegistration>&) {}
-  virtual void RegisterSqlModules(
-      TraceProcessorContext*,
-      std::vector<std::pair<std::string, std::string>>&) {}
-
- private:
-  void RegisterImporters(TraceProcessorContext* ctx, Destructible*) final {
-    static_cast<Self*>(this)->RegisterImporters(ctx);
-  }
-  void RegisterDataframes(TraceProcessorContext* ctx,
-                          Destructible*,
-                          std::vector<PluginDataframe>& tables) final {
-    static_cast<Self*>(this)->RegisterDataframes(ctx, tables);
-  }
-  void RegisterStaticTableFunctions(
-      TraceProcessorContext* ctx,
-      Destructible*,
-      std::vector<std::unique_ptr<StaticTableFunction>>& fns) final {
-    static_cast<Self*>(this)->RegisterStaticTableFunctions(ctx, fns);
-  }
-  void RegisterSqliteModules(
-      TraceProcessorContext* ctx,
-      Destructible*,
-      std::vector<SqliteModuleRegistration>& modules) final {
-    static_cast<Self*>(this)->RegisterSqliteModules(ctx, modules);
-  }
-  void RegisterSqlModules(
-      TraceProcessorContext* ctx,
-      Destructible*,
-      std::vector<std::pair<std::string, std::string>>& modules) final {
-    static_cast<Self*>(this)->RegisterSqlModules(ctx, modules);
+  // Returns a dependency plugin by type. T must be one of the Deps.
+  // The index is resolved at compile time from the parameter pack position.
+  template <typename T>
+  T* dependency() {
+    return static_cast<T*>(
+        resolved_deps_[core::TypeSet<Deps...>::template GetTypeIndex<T>()]);
   }
 };
 
@@ -280,11 +183,21 @@
                      size_t n_deps);
 };
 
-PluginRegistration* GetPluginRegistrations();
+// The result of topological sorting: plugin factories in dependency order
+// with pre-resolved dep indices. Computed once (statically) and shared by
+// all TraceProcessorImpl instances.
+struct PluginSet {
+  struct Entry {
+    PluginRegistration::Factory factory;
+    // Indices into the PluginSet::entries vector for this plugin's deps.
+    std::vector<size_t> dep_indices;
+  };
+  std::vector<Entry> entries;
+};
 
-// Collects all registered plugins, topologically sorts them by dependencies,
-// and instantiates them in order.
-std::vector<std::unique_ptr<PluginBase>> CreatePlugins();
+// Returns the singleton PluginSet. The topological sort and dep resolution
+// happen once on first call; subsequent calls return the cached result.
+const PluginSet& GetPluginSet();
 
 // Suppresses -Wglobal-constructors which is Clang-specific. GCC and MSVC
 // do not have this warning so the macros are no-ops on those compilers.
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index d7359c2..7383b82 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -510,13 +510,26 @@
 
 TraceProcessorImpl::TraceProcessorImpl(const Config& cfg)
     : TraceProcessorStorageImpl(cfg), config_(cfg) {
-  // Initialize plugins: create instances, allocate storage, register importers.
-  for (auto& p : CreatePlugins()) {
-    PluginEntry entry;
-    entry.storage = p->CreateStorage(context());
-    entry.plugin = std::move(p);
-    entry.plugin->RegisterImporters(context(), entry.storage.get());
-    plugins_.push_back(std::move(entry));
+  // Initialize plugins using the statically pre-computed PluginSet.
+  // Dep indices are resolved once at static init time; here we just
+  // instantiate, resolve dep pointers, and register importers.
+  {
+    const PluginSet& pset = GetPluginSet();
+    plugins_.reserve(pset.entries.size());
+    for (const auto& pse : pset.entries) {
+      plugins_.push_back(pse.factory());
+    }
+    for (size_t i = 0; i < pset.entries.size(); ++i) {
+      auto& p = *plugins_[i];
+      p.trace_context_ = context();
+      for (size_t dep_idx : pset.entries[i].dep_indices) {
+        p.resolved_deps_.push_back(plugins_[dep_idx].get());
+      }
+    }
+    // Let plugins register trace readers and proto importer modules.
+    for (auto& p : plugins_) {
+      p->RegisterImporters(*context()->reader_registry);
+    }
   }
   context()->register_additional_proto_modules = &RegisterAdditionalModules;
 
@@ -722,16 +735,16 @@
   // Also finalize plugin-owned tables.
   {
     std::vector<PluginDataframe> plugin_tables;
-    for (const auto& e : plugins_) {
-      e.plugin->RegisterDataframes(context(), e.storage.get(), plugin_tables);
+    for (auto& p : plugins_) {
+      p->RegisterDataframes(plugin_tables);
     }
     for (const auto& table : plugin_tables) {
       table.dataframe->Finalize();
     }
   }
 
-  // Stage 4: register plugin SQL modules and prepare the engine for queries.
-  IncludeAfterEofPrelude(context(), engine_.get(), plugins_);
+  // Stage 4: prepare the engine for queries.
+  IncludeAfterEofPrelude(engine_.get());
   sqlite_objects_post_prelude_ = engine_->SqliteRegisteredObjectCount();
 
   return base::OkStatus();
@@ -1277,10 +1290,10 @@
   std::vector<PerfettoSqlEngine::StaticTable> tables = GetStaticTables(storage);
   std::vector<PluginDataframe> plugin_dataframes;
   std::vector<SqliteModuleRegistration> sqlite_modules;
-  for (const auto& e : plugins) {
-    e.plugin->RegisterDataframes(context, e.storage.get(), plugin_dataframes);
-    e.plugin->RegisterStaticTableFunctions(context, e.storage.get(), functions);
-    e.plugin->RegisterSqliteModules(context, e.storage.get(), sqlite_modules);
+  for (auto& p : plugins) {
+    p->RegisterDataframes(plugin_dataframes);
+    p->RegisterStaticTableFunctions(functions);
+    p->RegisterSqliteModules(sqlite_modules);
   }
   for (auto& df : plugin_dataframes) {
     tables.push_back({df.dataframe, std::move(df.name)});
@@ -1510,7 +1523,7 @@
   }
 
   if (notify_eof_called) {
-    IncludeAfterEofPrelude(context, engine.get(), plugins);
+    IncludeAfterEofPrelude(engine.get());
   }
 
   for (const auto& metric : sql_metrics) {
@@ -1522,30 +1535,7 @@
   return engine;
 }
 
-void TraceProcessorImpl::IncludeAfterEofPrelude(
-    TraceProcessorContext* context,
-    PerfettoSqlEngine* engine,
-    const std::vector<PluginEntry>& plugins) {
-  // Collect SQL modules from plugins and merge them into existing packages
-  // (or create new ones).
-  std::vector<std::pair<std::string, std::string>> plugin_modules;
-  for (const auto& e : plugins) {
-    e.plugin->RegisterSqlModules(context, e.storage.get(), plugin_modules);
-  }
-  for (auto& mod : plugin_modules) {
-    std::string pkg_name = sql_modules::GetPackageName(mod.first);
-    auto* existing = engine->FindPackage(pkg_name);
-    if (existing) {
-      existing->modules.Insert(mod.first,
-                               {std::move(mod.second), /*included=*/false});
-    } else {
-      sql_modules::RegisteredPackage new_pkg;
-      new_pkg.modules.Insert(mod.first,
-                             {std::move(mod.second), /*included=*/false});
-      engine->RegisterPackage(pkg_name, std::move(new_pkg));
-    }
-  }
-
+void TraceProcessorImpl::IncludeAfterEofPrelude(PerfettoSqlEngine* engine) {
   auto result = engine->Execute(SqlSource::FromTraceProcessorImplementation(
       "INCLUDE PERFETTO MODULE prelude.after_eof.*"));
   if (!result.status().ok()) {
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index e178c44..2b61a93 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -43,7 +43,6 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_processor_storage_impl.h"
-#include "src/trace_processor/types/destructible.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/descriptors.h"
 
@@ -143,11 +142,6 @@
   // Needed for iterators to be able to access the context.
   friend class IteratorImpl;
 
-  struct PluginEntry {
-    std::unique_ptr<PluginBase> plugin;
-    std::unique_ptr<Destructible> storage;
-  };
-
   bool IsRootMetricField(const std::string& metric_name);
 
   void CacheBoundsAndBuildTable();
@@ -163,7 +157,7 @@
     TraceProcessor* trace_processor;
     bool notify_eof_called;
     std::pair<int64_t, int64_t> cached_trace_bounds;
-    const std::vector<PluginEntry>& plugins;
+    std::vector<std::unique_ptr<PluginBase>>& plugins;
   };
 
   static std::unique_ptr<PerfettoSqlEngine> InitPerfettoSqlEngine(
@@ -178,14 +172,12 @@
                              const Config& config,
                              PerfettoSqlEngine* engine);
 
-  static void IncludeAfterEofPrelude(TraceProcessorContext*,
-                                     PerfettoSqlEngine*,
-                                     const std::vector<PluginEntry>& plugins);
+  static void IncludeAfterEofPrelude(PerfettoSqlEngine*);
 
   const Config config_;
 
   // Registered plugins, topologically sorted by dependency order.
-  std::vector<PluginEntry> plugins_;
+  std::vector<std::unique_ptr<PluginBase>> plugins_;
 
   std::unique_ptr<PerfettoSqlEngine> engine_;
 
diff --git a/src/trace_processor/trace_reader_registry.h b/src/trace_processor/trace_reader_registry.h
index 34039df..9a61713 100644
--- a/src/trace_processor/trace_reader_registry.h
+++ b/src/trace_processor/trace_reader_registry.h
@@ -19,6 +19,7 @@
 
 #include <functional>
 #include <memory>
+#include <utility>
 
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/status_or.h"
@@ -45,6 +46,16 @@
     });
   }
 
+  // Registers a trace reader factory that captures its own state (e.g. a
+  // plugin's `this` pointer). The TraceProcessorContext* passed at creation
+  // time is ignored by the wrapper.
+  void RegisterPluginTraceReader(
+      TraceType trace_type,
+      std::function<std::unique_ptr<ChunkedTraceReader>()> factory) {
+    RegisterFactory(trace_type, [f = std::move(factory)](
+                                    TraceProcessorContext*) { return f(); });
+  }
+
   // Creates a new `ChunkedTraceReader` instance for the given `type`. Returns
   // an error if no mapping has been previously registered.
   base::StatusOr<std::unique_ptr<ChunkedTraceReader>> CreateTraceReader(