Skip to content

Commit

Permalink
Add single method to fetch profiler
Browse files Browse the repository at this point in the history
The MRI probe always initialized the profiler needed to actually profile
the garbage collection. If `enable_gc_instrumentation` is set to
`false`, which it is by default, it has some overhead that's not needed.

Instead it should use the `NilProfiler` like we had in the Transaction
class.

Move this logic to a new `GarbageCollection` wrapper module in which the
`GarbageCollection.profiler` will check the config option value and
return the suitable profiler.

Part of #868
  • Loading branch information
tombruijn committed Aug 5, 2022
1 parent 66821df commit b3a163b
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 85 deletions.
6 changes: 6 additions & 0 deletions .changesets/fix-garbage-collection-profiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: "patch"
type: "fix"
---

Fix the MRI probe using the Garbage Collection profiler instead of the NilProfiler when garbage collection instrumentation is not enabled for MRI probe. This caused unnecessary overhead.
2 changes: 1 addition & 1 deletion lib/appsignal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def collect_environment_metadata
require "appsignal/probes"
require "appsignal/marker"
require "appsignal/minutely"
require "appsignal/garbage_collection_profiler"
require "appsignal/garbage_collection"
require "appsignal/integrations/railtie" if defined?(::Rails)
require "appsignal/transaction"
require "appsignal/version"
Expand Down
81 changes: 81 additions & 0 deletions lib/appsignal/garbage_collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

module Appsignal
# @api private
module GarbageCollection
# Return the GC profiler wrapper.
#
# Returns {Profiler} if `enable_gc_instrumentation` is enabled and
# {NilProfiler} if it is disabled.
#
# GC profiling is disabled by default due to the overhead it causes. Do not
# enable this in production for long periods of time.
def self.profiler(appsignal = Appsignal)
@profiler ||=
if appsignal.config[:enable_gc_instrumentation]
Profiler.new
else
NilProfiler.new
end
end

# Unset the currently cached profiler
#
# @return [void]
def self.clear_profiler!
@profiler = nil
end

# A wrapper around Ruby's `GC::Profiler` that tracks garbage collection
# time, while clearing `GC::Profiler`'s total_time to make sure it doesn't
# leak memory by keeping garbage collection run samples in memory.
class Profiler
def self.lock
@lock ||= Mutex.new
end

def initialize
@total_time = 0
end

# Whenever {#total_time} is called, the current `GC::Profiler#total_time`
# gets added to `@total_time`, after which `GC::Profiler.clear` is called
# to prevent it from leaking memory. A class-level lock is used to make
# sure garbage collection time is never counted more than once.
#
# Whenever `@total_time` gets above two billion milliseconds (about 23
# days), it's reset to make sure the result fits in a signed 32-bit
# integer.
#
# @return [Integer]
def total_time
lock.synchronize do
@total_time += (internal_profiler.total_time * 1000).round
internal_profiler.clear
end

@total_time = 0 if @total_time > 2_000_000_000

@total_time
end

private

def internal_profiler
GC::Profiler
end

def lock
self.class.lock
end
end

# A dummy profiler that always returns 0 as the total time. Used when GC
# profiler is disabled.
class NilProfiler
def total_time
0
end
end
end
end
61 changes: 0 additions & 61 deletions lib/appsignal/garbage_collection_profiler.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/appsignal/probes/mri.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def self.dependencies_present?
defined?(::RubyVM) && ::RubyVM.respond_to?(:stat)
end

def initialize(appsignal: Appsignal, gc_profiler: Appsignal::GarbageCollectionProfiler.new)
def initialize(appsignal: Appsignal, gc_profiler: Appsignal::GarbageCollection.profiler)
Appsignal.logger.debug("Initializing VM probe")
@appsignal = appsignal
@gc_profiler = gc_profiler
Expand Down
3 changes: 1 addition & 2 deletions lib/appsignal/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ def clear_current_transaction!
end

def garbage_collection_profiler
@garbage_collection_profiler ||=
Appsignal.config[:enable_gc_instrumentation] ? Appsignal::GarbageCollectionProfiler.new : NilGarbageCollectionProfiler.new
Appsignal::GarbageCollection.profiler
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
describe Appsignal::GarbageCollectionProfiler do
describe Appsignal::GarbageCollection do
describe ".profiler" do
let(:appsignal) { class_double("Appsignal") }
before do
# Unset the internal memoized variable to avoid state leaking
described_class.clear_profiler!
end

context "when GC instrumentation is disabled" do
let(:config) { Appsignal::Config.new("production", Dir.pwd, :enable_gc_instrumentation => false) }

it "returns the NilProfiler" do
allow(appsignal).to receive(:config).and_return(config)

expect(described_class.profiler(appsignal)).to be_a(Appsignal::GarbageCollection::NilProfiler)
end
end

context "when GC profiling is enabled" do
let(:config) { Appsignal::Config.new("production", Dir.pwd, :enable_gc_instrumentation => true) }

it "returns the Profiler" do
allow(appsignal).to receive(:config).and_return(config)

expect(described_class.profiler(appsignal)).to be_a(Appsignal::GarbageCollection::Profiler)
end
end
end
end

describe Appsignal::GarbageCollection::Profiler do
let(:internal_profiler) { FakeGCProfiler.new }
let(:profiler) { described_class.new }

Expand Down Expand Up @@ -52,7 +82,7 @@

2.times do
threads << Thread.new do
profiler = Appsignal::GarbageCollectionProfiler.new
profiler = Appsignal::GarbageCollection::Profiler.new
results << profiler.total_time
end
end
Expand All @@ -63,7 +93,7 @@
end
end

describe Appsignal::NilGarbageCollectionProfiler do
describe Appsignal::GarbageCollection::NilProfiler do
let(:profiler) { described_class.new }

describe "#total_time" do
Expand Down
17 changes: 0 additions & 17 deletions spec/lib/appsignal/transaction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -792,23 +792,6 @@ def to_s
end
end

describe "#garbage_collection_profiler" do
before { Appsignal::Transaction.instance_variable_set(:@garbage_collection_profiler, nil) }

it "returns the NilGarbageCollectionProfiler" do
expect(Appsignal::Transaction.garbage_collection_profiler).to be_a(Appsignal::NilGarbageCollectionProfiler)
end

context "when gc profiling is enabled" do
before { Appsignal.config.config_hash[:enable_gc_instrumentation] = true }
after { Appsignal.config.config_hash[:enable_gc_instrumentation] = false }

it "returns the GarbageCollectionProfiler" do
expect(Appsignal::Transaction.garbage_collection_profiler).to be_a(Appsignal::GarbageCollectionProfiler)
end
end
end

describe "#start_event" do
it "starts the event in the extension" do
expect(transaction.ext).to receive(:start_event).with(0).and_call_original
Expand Down

0 comments on commit b3a163b

Please sign in to comment.