From ae0bae022405aa1594b6a9cd7960b1584469149f Mon Sep 17 00:00:00 2001 From: Kir Shatrov Date: Mon, 6 Apr 2020 14:49:21 +0100 Subject: [PATCH] WIP - usage tracking --- lib/identity_cache.rb | 1 + lib/identity_cache/cached/prefetcher.rb | 6 +- lib/identity_cache/query_api.rb | 1 + lib/identity_cache/tracking.rb | 70 ++++++++++++++ lib/identity_cache/with_primary_index.rb | 2 + test/tracking_test.rb | 116 +++++++++++++++++++++++ 6 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 lib/identity_cache/tracking.rb create mode 100644 test/tracking_test.rb diff --git a/lib/identity_cache.rb b/lib/identity_cache.rb index 87520f4e..86173250 100644 --- a/lib/identity_cache.rb +++ b/lib/identity_cache.rb @@ -40,6 +40,7 @@ require "identity_cache/fallback_fetcher" require 'identity_cache/without_primary_index' require 'identity_cache/with_primary_index' +require "identity_cache/tracking" module IdentityCache extend ActiveSupport::Concern diff --git a/lib/identity_cache/cached/prefetcher.rb b/lib/identity_cache/cached/prefetcher.rb index a5c19124..74fc6642 100644 --- a/lib/identity_cache/cached/prefetcher.rb +++ b/lib/identity_cache/cached/prefetcher.rb @@ -42,8 +42,10 @@ def fetch_association(load_strategy, klass, association, records, &block) return yield end - cached_association = klass.cached_association(association) - cached_association.fetch_async(load_strategy, records, &block) + IdentityCache::Tracking.skip_object_tracking do + cached_association = klass.cached_association(association) + cached_association.fetch_async(load_strategy, records, &block) + end end end end diff --git a/lib/identity_cache/query_api.rb b/lib/identity_cache/query_api.rb index 47d10ecc..4e911384 100644 --- a/lib/identity_cache/query_api.rb +++ b/lib/identity_cache/query_api.rb @@ -168,6 +168,7 @@ def was_new_record? # :nodoc: def fetch_recursively_cached_association(ivar_name, dehydrated_ivar_name, association_name) # :nodoc: assoc = association(association_name) + IdentityCache::Tracking.track_association_accessed(self, association_name) if assoc.klass.should_use_cache? && !assoc.loaded? if instance_variable_defined?(ivar_name) instance_variable_get(ivar_name) diff --git a/lib/identity_cache/tracking.rb b/lib/identity_cache/tracking.rb new file mode 100644 index 00000000..dcdd0e29 --- /dev/null +++ b/lib/identity_cache/tracking.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module IdentityCache + module Tracking + TrackedObject = Struct.new(:object, :includes, :caller, :accessed_associations) + + extend self + + def tracked_objects + Thread.current[:idc_tracked_objects] ||= {} + end + + def reset_tracked_objects + Thread.current[:idc_tracked_objects] = {} + end + + def track_object(object, includes) + return unless object_tracking_enabled + locations = caller(1, 20) + tracked_objects[object] = TrackedObject.new(object, Array(includes), locations, Set.new) + end + + def track_association_accessed(object, association_name) + return unless object_tracking_enabled + obj = self.tracked_objects[object] + obj.accessed_associations << association_name if obj + end + + def instrument_and_reset_tracked_objects + tracked_objects.each do |_, to| + ActiveSupport::Notifications.instrument('object_track.identity_cache', { + object: to.object, + accessed_associations: to.accessed_associations, + caller: to.caller + }) + end + reset_tracked_objects + end + + def with_object_tracking_and_instrumentation + begin + with_object_tracking { yield } + ensure + instrument_and_reset_tracked_objects + end + end + + def with_object_tracking(enabled: true) + begin + orig = object_tracking_enabled + self.object_tracking_enabled = enabled + yield + ensure + self.object_tracking_enabled = orig + end + end + + def skip_object_tracking + with_object_tracking(enabled: false) { yield } + end + + def object_tracking_enabled + Thread.current[:object_tracking_enabled] + end + + def object_tracking_enabled=(value) + Thread.current[:object_tracking_enabled] = value + end + end +end diff --git a/lib/identity_cache/with_primary_index.rb b/lib/identity_cache/with_primary_index.rb index 76400e15..b4d3a6f8 100644 --- a/lib/identity_cache/with_primary_index.rb +++ b/lib/identity_cache/with_primary_index.rb @@ -103,6 +103,7 @@ def fetch_by_id(id, includes: nil) ensure_base_model raise_if_scoped record = cached_primary_index.fetch(id) + IdentityCache::Tracking.track_object(record, includes) if record prefetch_associations(includes, [record]) if record && includes record end @@ -123,6 +124,7 @@ def fetch_multi(*ids, includes: nil) raise_if_scoped ids.flatten!(1) records = cached_primary_index.fetch_multi(ids) + records.each { |record| IdentityCache::Tracking.track_object(record, includes) } prefetch_associations(includes, records) if includes records end diff --git a/test/tracking_test.rb b/test/tracking_test.rb new file mode 100644 index 00000000..d178f09b --- /dev/null +++ b/test/tracking_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +require "test_helper" + +class SaveTest < IdentityCache::TestCase + def setup + super + Item.cache_index(:title, unique: true) + Item.cache_has_many(:normalized_associated_records, embed: true) + + @record = Item.create(title: 'bob') + @record.normalized_associated_records.create! + end + + def test_fetch_index_tracks_object_no_accessed_associations + fill_cache + + captured = 0 + subscriber = ActiveSupport::Notifications.subscribe('object_track.identity_cache') do |_, _, _, _, payload| + captured += 1 + assert_same_record(@record, payload[:object]) + assert_equal [].to_set, payload[:accessed_associations] + assert_kind_of Array, payload[:caller] + end + + IdentityCache::Tracking.with_object_tracking_and_instrumentation do + Item.fetch_by_title('bob') + end + assert_equal 1, captured + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_fetch_index_tracks_object_with_accessed_associations + fill_cache + + captured = 0 + subscriber = ActiveSupport::Notifications.subscribe('object_track.identity_cache') do |_, _, _, _, payload| + captured += 1 + assert_same_record(@record, payload[:object]) + assert_equal [:normalized_associated_records].to_set, payload[:accessed_associations] + assert_kind_of Array, payload[:caller] + end + + IdentityCache::Tracking.with_object_tracking_and_instrumentation do + item = Item.fetch_by_title('bob') + item.fetch_normalized_associated_records + end + assert_equal 1, captured + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + + def test_fetch_tracks_object_no_accessed_associations + fill_cache + + captured = 0 + subscriber = ActiveSupport::Notifications.subscribe('object_track.identity_cache') do |_, _, _, _, payload| + captured += 1 + assert_same_record(@record, payload[:object]) + assert_equal [].to_set, payload[:accessed_associations] + assert_kind_of Array, payload[:caller] + end + + IdentityCache::Tracking.with_object_tracking_and_instrumentation do + item = Item.fetch(@record.id) + end + assert_equal 1, captured + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_fetch_tracks_object_with_accessed_associations + fill_cache + + captured = 0 + subscriber = ActiveSupport::Notifications.subscribe('object_track.identity_cache') do |_, _, _, _, payload| + captured += 1 + assert_same_record(@record, payload[:object]) + assert_equal [:normalized_associated_records].to_set, payload[:accessed_associations] + assert_kind_of Array, payload[:caller] + end + + IdentityCache::Tracking.with_object_tracking_and_instrumentation do + item = Item.fetch(@record.id) + item.fetch_normalized_associated_records + end + assert_equal 1, captured + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_does_not_track_objects_unless_enabled + fill_cache + + subscriber = ActiveSupport::Notifications.subscribe('object_track.identity_cache') do |_, _, _, _, payload| + refute + end + + item = Item.fetch_by_title('bob') + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + private + + def fill_cache + item = Item.fetch_by_title('bob') + assert item + item.fetch_normalized_associated_records + end + + def assert_same_record(expected, actual) + assert_equal expected.id, actual.id + end +end