From 693868d8dd2d136776bfad140c997eb18237bbfc Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Jul 2026 14:51:20 +0200 Subject: [PATCH 1/2] perf(datasource_toolkit): Cache collection name and lookups Serializing a list re-resolved collections per record by walking the decorator stacks. Profiling a 100-row related list showed ~54% of wall time in CollectionDecorator#name (re-chaining child_collection.name on every call) and DatasourceDecorator#get_collection (re-descending the datasource stack on every call), invoked per record and per relation by the JSON:API serializer. Memoize the immutable results: - CollectionDecorator#name caches into @memoized_name (not @name, which the base Collection already holds from the mis-ordered super). - DatasourceDecorator#get_collection caches the decorator by name. - ForestSerializer#id caches primary keys per type instead of resolving the collection and its keys for every record. No behavioural change. On the bench app, list serialization drops ~7-10x (related list of 100 rows: 58ms to 9.6ms; root list of 100: 203ms to 27ms). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../forest_admin_agent/serializer/forest_serializer.rb | 6 +++--- .../decorators/collection_decorator.rb | 4 +++- .../decorators/datasource_decorator.rb | 9 ++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb index 0f2ba7d0d..5b6179d4f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb @@ -25,10 +25,10 @@ def type end def id - forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection( - @options[:class_name].gsub('::', '__') + @@primary_keys ||= {} + primary_keys = @@primary_keys[type] ||= ForestAdminDatasourceToolkit::Utils::Schema.primary_keys( + ForestAdminAgent::Facades::Container.datasource.get_collection(type) ) - primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(forest_collection) primary_keys.map { |key| @object[key] }.join('|') end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb index bbc561079..e72573de9 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb @@ -28,7 +28,9 @@ def schema end def name - @child_collection.name + # `@name` is taken by the base Collection (set via the mis-ordered `super` in initialize), + # so memoize under a dedicated ivar. + @memoized_name ||= @child_collection.name # rubocop:disable Naming/MemoizedInstanceVariableName end def execute(caller, name, data, filter = nil) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb index 5285deb02..8eb4be797 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb @@ -22,12 +22,11 @@ def live_query_connections end def get_collection(name) - collection = @child_datasource.get_collection(name) - unless @decorators.key?(collection.name) - @decorators[collection.name] = @collection_decorator_class.new(collection, self) + @collections_by_name ||= {} + @collections_by_name[name] ||= begin + collection = @child_datasource.get_collection(name) + @decorators[collection.name] ||= @collection_decorator_class.new(collection, self) end - - @decorators[collection.name] end def render_chart(caller, name, parameters = {}) From ba5fa457f5bbe6e8c2e875f3f5e424e9af6fcbc7 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Jul 2026 15:58:36 +0200 Subject: [PATCH 2/2] fix(agent): Reset serializer primary-key cache on reload The `@@primary_keys` memo added to `ForestSerializer#id` is a class variable that outlives `AgentFactory#reload!` (which rebuilds the datasource via `container_replace`). If a collection's primary keys change across a reload, records would keep serializing stale JSON:API `id` values until process restart. Reset it in `reload!` next to the existing route-cache reset. The other two memoizations (`CollectionDecorator#name`, `DatasourceDecorator#get_collection`) live on the decorator instances, which are rebuilt on reload, so they need no explicit reset. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/forest_admin_agent/builder/agent_factory.rb | 2 ++ .../serializer/forest_serializer.rb | 4 ++++ .../forest_admin_agent/builder/agent_factory_spec.rb | 11 +++++++++++ 3 files changed, 17 insertions(+) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index 1983a38ef..e4f7fba41 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -99,6 +99,8 @@ def reload! # Reset route cache before sending schema to ensure routes are recomputed with all customizations ForestAdminAgent::Http::Router.reset_cached_routes! + # The datasource was rebuilt, so the serializer's primary-key cache may be stale + ForestAdminAgent::Serializer::ForestSerializer.reset_cache! @logger.log('Info', 'route cache cleared due to agent reload') send_schema diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb index 5b6179d4f..1da0e4c89 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb @@ -24,6 +24,10 @@ def type @@class_names[class_name] ||= class_name.gsub('::', '__') end + def self.reset_cache! + @@primary_keys = {} + end + def id @@primary_keys ||= {} primary_keys = @@primary_keys[type] ||= ForestAdminDatasourceToolkit::Utils::Schema.primary_keys( diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb index 342e65b3a..fea917258 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb @@ -136,6 +136,17 @@ module Builder expect(instance.customizer).to have_received(:reload!) end + it 'resets the serializer primary-key cache' do + instance = described_class.instance + allow(instance).to receive(:send_schema) + allow(instance.customizer).to receive(:reload!) + allow(ForestAdminAgent::Serializer::ForestSerializer).to receive(:reset_cache!) + + instance.reload! + + expect(ForestAdminAgent::Serializer::ForestSerializer).to have_received(:reset_cache!) + end + it 'add datasource to the container' do allow(described_class.instance).to receive(:send_schema) described_class.instance.reload!