Skip to content

Commit

Permalink
Guage RubyVM::YJIT.runtime_stats if YJIT enabled
Browse files Browse the repository at this point in the history
YJIT was deemed production ready in Ruby 3.2. YJIT includes its own
[runtime
stats](https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md#other-statistics)
which would be useful to guage.

Currently, this is only enabled when YJIT is enabled (running Ruby with
the "--yjit" flag), but NOT when the "--yjit-stats" is _also_ passed.

YJIT, by default, has the following runtime stats:

```
$ RUBYOPT='--yjit' ruby -e 'pp RubyVM::YJIT.runtime_stats' =>
{
  :inline_code_size=>0,
  :outlined_code_size=>116,
  :freed_page_count=>0,
  :freed_code_size=>0,
  :live_page_count=>1,
  :code_gc_count=>0,
  :code_region_size=>16384,
  :object_shape_count=>236
}

With the `--yjit-stats` flag, it returns 325 separate stats:

```
$ RUBYOPT="--yjit --yjit-stats" ruby -e 'pp RubyVM::YJIT.runtime_stats.keys.length' =>
325
```

And not all of them are numeric:

```
$ RUBYOPT="--yjit --yjit-stats" ruby -e 'pp
RubyVM::YJIT.runtime_stats.keys.select { |k| !kind_of?(Numeric) }.any?' =>
true
```
  • Loading branch information
HeyNonster committed Mar 24, 2023
1 parent 35bf597 commit 2ca71db
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 5 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/test-yjit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Test YJIT
on: [push]
jobs:
test-yjit:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
ruby:
- '3.2'
# ADD NEW RUBIES HERE
name: Test (${{ matrix.os }}, ${{ matrix.ruby }})
runs-on: ${{ matrix.os }}
env:
RUBYOPT: "--yjit"
SKIP_SIMPLECOV: 1
DD_INSTRUMENTATION_TELEMETRY_ENABLED: false
steps:
- uses: actions/checkout@v3
# bundler appears to match both prerelease and release rubies when we
# want the former only. relax the constraint to allow any version for
# head rubies
- if: ${{ matrix.ruby == 'head' }}
run: sed -i~ -e '/spec\.required_ruby_version/d' ddtrace.gemspec
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
bundler: latest # needed to fix issue with steep on Ruby 3.0/3.1
cache-version: v2 # bump this to invalidate cache
- run: bundle exec rake spec:yjit
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ namespace :spec do
t.rspec_opts = args.to_a.join(' ')
end

RSpec::Core::RakeTask.new(:yjit) do |t, args|
t.pattern = 'spec/datadog/core/runtime/metrics_spec.rb'
t.rspec_opts = args.to_a.join(' ')
end

# rails_semantic_logger is the dog at the dog park that doesnt play nicely with other
# logging gems, aka it tries to bite/monkeypatch them, so we have to put it in its own appraisal and rake task
# in order to isolate its effects for rails logs auto injection
Expand Down
12 changes: 7 additions & 5 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -2780,16 +2780,18 @@ See the [Dogstatsd documentation](https://www.rubydoc.info/github/DataDog/dogsta

The stats are VM specific and will include:

| Name | Type | Description | Available on |
| -------------------------- | ------- | -------------------------------------------------------- | ------------ |
| `runtime.ruby.class_count` | `gauge` | Number of classes in memory space. | CRuby |
| `runtime.ruby.gc.*` | `gauge` | Garbage collection statistics: collected from `GC.stat`. | All runtimes |
| `runtime.ruby.thread_count` | `gauge` | Number of threads. | All runtimes |
| Name | Type | Description | Available on |
| -------------------------- | ------- | ------------------------------------------------------------- | ------------------ |
| `runtime.ruby.class_count` | `gauge` | Number of classes in memory space. | CRuby |
| `runtime.ruby.gc.*` | `gauge` | Garbage collection statistics: collected from `GC.stat`. | All runtimes |
| `runtime.ruby.yjit.*` | `gauge` | YJIT statistics collected from `RubyVM::YJIT.runtime_stats`. | CRuby (if enabled) |
| `runtime.ruby.thread_count` | `gauge` | Number of threads. | All runtimes |
| `runtime.ruby.global_constant_state` | `gauge` | Global constant cache generation. | CRuby ≤ 3.1 |
| `runtime.ruby.global_method_state` | `gauge` | [Global method cache generation.](https://tenderlovemaking.com/2015/12/23/inline-caching-in-mri.html) | [CRuby 2.x](https://docs.ruby-lang.org/en/3.0.0/NEWS_md.html#label-Implementation+improvements) |
| `runtime.ruby.constant_cache_invalidations` | `gauge` | Constant cache invalidations. | CRuby ≥ 3.2 |
| `runtime.ruby.constant_cache_misses` | `gauge` | Constant cache misses. | CRuby ≥ 3.2 |


In addition, all metrics include the following tags:

| Name | Description |
Expand Down
64 changes: 64 additions & 0 deletions lib/datadog/core/environment/yjit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module Datadog
module Core
module Environment
# Reports YJIT primitive runtime statistics.
module YJIT
module_function

# Inline code size
def inline_code_size
::RubyVM::YJIT.runtime_stats[:inline_code_size]
end

# Outlined code size
def outlined_code_size
::RubyVM::YJIT.runtime_stats[:outlined_code_size]
end

# GCed pages
def freed_page_count
::RubyVM::YJIT.runtime_stats[:freed_page_count]
end

# GCed code size
def freed_code_size
::RubyVM::YJIT.runtime_stats[:freed_code_size]
end

# Live pages
def live_page_count
::RubyVM::YJIT.runtime_stats[:live_page_count]
end

# Code GC count
def code_gc_count
::RubyVM::YJIT.runtime_stats[:code_gc_count]
end

# Size of memory region allocated for JIT code
def code_region_size
::RubyVM::YJIT.runtime_stats[:code_region_size]
end

# Total number of object shapes
def object_shape_count
::RubyVM::YJIT.runtime_stats[:object_shape_count]
end

def available?
# Not available when --yjit-stats is enabled.
#
# RubyVM::YJIT.runtime_stats contains 8 keys when `.stats_enabled?` is false.
# However, when `.stats_enabled?` is true `.runtime_stats` returns 325 different
# statistics, and not all of them are Numeric.
defined?(::RubyVM::YJIT) \
&& ::RubyVM::YJIT.enabled? \
&& ::RubyVM::YJIT.respond_to?(:runtime_stats) \
&& !::RubyVM::YJIT.stats_enabled?
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/datadog/core/runtime/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ module Metrics
METRIC_GLOBAL_METHOD_STATE = 'runtime.ruby.global_method_state'.freeze
METRIC_CONSTANT_CACHE_INVALIDATIONS = 'runtime.ruby.constant_cache_invalidations'.freeze
METRIC_CONSTANT_CACHE_MISSES = 'runtime.ruby.constant_cache_misses'.freeze
METRIC_YJIT_CODE_GC_COUNT = 'runtime.ruby.yjit.code_gc_count'.freeze
METRIC_YJIT_CODE_REGION_SIZE = 'runtime.ruby.yjit.code_region_size'.freeze
METRIC_YJIT_FREED_CODE_SIZE = 'runtime.ruby.yjit.freed_code_size'.freeze
METRIC_YJIT_FREED_PAGE_COUNT = 'runtime.ruby.yjit.freed_page_count'.freeze
METRIC_YJIT_INLINE_CODE_SIZE = 'runtime.ruby.yjit.inline_code_size'.freeze
METRIC_YJIT_LIVE_PAGE_COUNT = 'runtime.ruby.yjit.live_page_count'.freeze
METRIC_YJIT_OBJECT_SHAPE_COUNT = 'runtime.ruby.yjit.object_shape_count'.freeze
METRIC_YJIT_OUTLINED_CODE_SIZE = 'runtime.ruby.yjit.outlined_code_size'.freeze

TAG_SERVICE = 'service'.freeze
end
Expand Down
52 changes: 52 additions & 0 deletions lib/datadog/core/runtime/metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative '../environment/gc'
require_relative '../environment/thread_count'
require_relative '../environment/vm_cache'
require_relative '../environment/yjit'

module Datadog
module Core
Expand Down Expand Up @@ -78,6 +79,45 @@ def flush
)
end
end

# Only on Ruby >= 3.2
try_flush do
if Core::Environment::YJIT.available?
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_CODE_GC_COUNT,
Core::Environment::YJIT.code_gc_count
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_CODE_REGION_SIZE,
Core::Environment::YJIT.code_region_size
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_FREED_CODE_SIZE,
Core::Environment::YJIT.freed_code_size
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_FREED_PAGE_COUNT,
Core::Environment::YJIT.freed_page_count
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_INLINE_CODE_SIZE,
Core::Environment::YJIT.inline_code_size
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_LIVE_PAGE_COUNT,
Core::Environment::YJIT.live_page_count
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_OBJECT_SHAPE_COUNT,
Core::Environment::YJIT.object_shape_count
)
gauge_if_not_nil(
Core::Runtime::Ext::Metrics::METRIC_YJIT_OUTLINED_CODE_SIZE,
Core::Environment::YJIT.outlined_code_size
)
end
end
try_flush { yjit_metrics.each { |metric, value| gauge(metric, value) } if Core::Environment::YJIT.available? }
end

def gc_metrics
Expand All @@ -86,6 +126,13 @@ def gc_metrics
end.to_h
end

def yjit_metrics
Core::Environment::YJIT.runtime_stats.map do |k, v|
metric_name = prefixed_yjit_key(k)
[metric_name, v]
end.to_h
end

def try_flush
yield
rescue StandardError => e
Expand Down Expand Up @@ -127,6 +174,11 @@ def nested_gc_metric(prefix, k, v)
end
end

def prefixed_yjit_key(k)
prefixed_key = "#{Core::Runtime::Ext::Metrics::METRIC_YJIT_PREFIX}.#{k}"
to_metric_name(prefixed_key)
end

def to_metric_name(str)
str.downcase.gsub(/[-\s]/, '_')
end
Expand Down
52 changes: 52 additions & 0 deletions spec/datadog/core/runtime/metrics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,58 @@
end
end
end

context 'including YJIT stats' do
before do
skip('This feature is only supported in CRuby') unless PlatformHelpers.mri?
skip('Test only runs on Ruby >= 3.2') if RUBY_VERSION < '3.2.'
end

context 'with YJIT enabled and RubyVM::YJIT.stats_enabled? false' do
before do
unless Datadog::Core::Environment::YJIT.available?
skip('Test only runs with YJIT enabled and RubyVM::YJIT.stats_enabled? false')
end
allow(runtime_metrics).to receive(:gauge)
end

it do
flush

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_CODE_GC_COUNT, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_CODE_REGION_SIZE, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_FREED_CODE_SIZE, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_FREED_PAGE_COUNT, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_INLINE_CODE_SIZE, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_LIVE_PAGE_COUNT, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_OBJECT_SHAPE_COUNT, kind_of(Numeric))
.once

expect(runtime_metrics).to have_received(:gauge)
.with(Datadog::Core::Runtime::Ext::Metrics::METRIC_YJIT_OUTLINED_CODE_SIZE, kind_of(Numeric))
.once
end
end
end
end

it_behaves_like 'a flush of all runtime metrics'
Expand Down

0 comments on commit 2ca71db

Please sign in to comment.