Repository: jhawthorn/vernier (Ruby)

Issue:
[feat] Support user-supplied metadata options in Ruby API
              Should we also support this in the Ruby API? i.e. a metadata hash option for `Vernier.profile` and `Vernier.start_profile`. We should also update [these options](https://github.com/jhawthorn/vernier/blob/432ea5f65f8e846fcdb4d7e2b3c554a6f75e906c/README.md?plain=1#L127-L135) in that case.

_Originally posted by @joshuay03 in https://github.com/jhawthorn/vernier/issues/140#issuecomment-2753839986_
            


Discussion:
@joshuay03 you said you'd take this? Thanks!
Thank for this @joshuay03  - WDYT about cutting a new version to release these? Anything else we should include?
> WDYT about cutting a new version to release these? Anything else we should include?

That would be up to @jhawthorn 😃 Nothing else to include, I think the current state is releasable 👍🏽
I just cut 1.7.0 🚀
I plan to merge https://github.com/jhawthorn/vernier/pull/117 for the next version so that will likely be a little more experimental
> I just cut 1.7.0 🚀

Do we need a release to go along with the tag?



Patch:
diff --git a/README.md b/README.md
index 75123faa..dc4c43bf 100644
--- a/README.md
+++ b/README.md
@@ -133,8 +133,9 @@ ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "i
 | `mode`                | N/A                           | Sampling mode: `:wall`, `:retained`, or `:custom`.            | `:wall` (`:wall`)            |
 | `out`                 | N/A                           | File to write the profile to.                                 | N/A (Auto-generated)         |
 | `interval`            | `vernier_interval`            | Sampling interval (µs). Only in `:wall` mode.                 | `500` (`200`)                |
-| `allocation_interval` | `vernier_allocation_interval` | Allocation sampling interval. Only in `:wall` mode.           | `0` (disabled) (`200`)       |
+| `allocation_interval` | `vernier_allocation_interval` | Allocation sampling interval. Only in `:wall` mode.           | `0`/disabled (`200`)       |
 | `gc`                  | N/A                           | Run full GC cycle before profiling. Only in `:retained` mode. | `true` (N/A)                 |
+| `metadata`            | N/A                           | Metadata key-value pairs to include in the profile.           | `{}` (N/A)                   |
 
 ## Development
 
diff --git a/lib/vernier/autorun.rb b/lib/vernier/autorun.rb
index a8681272..f1b80dcc 100644
--- a/lib/vernier/autorun.rb
+++ b/lib/vernier/autorun.rb
@@ -15,21 +15,21 @@ class << self
     @options.transform_keys! do |key|
       key.sub(/\AVERNIER_/, "").downcase.to_sym
     end
-    if @options[:metadata]
-      @options[:user_metadata] = JSON.parse(Base64.decode64(@options[:metadata])).to_h do |k, v|
-        [k.to_sym, v]
-      end
-    end
     @options.freeze
 
     def self.start
       interval = options.fetch(:interval, 500).to_i
       allocation_interval = options.fetch(:allocation_interval, 0).to_i
       hooks = options.fetch(:hooks, "").split(",")
+      metadata = if options[:metadata]
+        JSON.parse(Base64.decode64(@options[:metadata])).to_h { |k, v| [k.to_sym, v] }
+      else
+        {}
+      end
 
       STDERR.puts("starting profiler with interval #{interval} and allocation interval #{allocation_interval}")
 
-      @collector = Vernier::Collector.new(:wall, interval:, allocation_interval:, hooks:)
+      @collector = Vernier::Collector.new(:wall, interval:, allocation_interval:, hooks:, metadata:)
       @collector.start
     end
 
@@ -37,7 +37,6 @@ def self.stop
       result = @collector.stop
       @collector = nil
 
-      result.meta[:user_metadata] = options[:user_metadata]
       output_path = options[:output]
       unless output_path
         output_dir = options[:output_dir]
diff --git a/lib/vernier/collector.rb b/lib/vernier/collector.rb
index d1f010b9..7662eca9 100644
--- a/lib/vernier/collector.rb
+++ b/lib/vernier/collector.rb
@@ -25,6 +25,8 @@ def initialize(mode, options = {})
       @hooks.each do |hook|
         hook.enable
       end
+
+      @user_metadata = options[:metadata] || {}
     end
 
     private def add_hook(hook)
@@ -79,6 +81,7 @@ def stop
       result.meta[:mode] = @mode
       result.meta[:out] = @out
       result.meta[:gc] = @gc
+      result.meta[:user_metadata] = @user_metadata
 
       result.stack_table = stack_table
       @thread_names.finish
diff --git a/lib/vernier/output/firefox.rb b/lib/vernier/output/firefox.rb
index dd33f77c..8110ef9d 100644
--- a/lib/vernier/output/firefox.rb
+++ b/lib/vernier/output/firefox.rb
@@ -149,7 +149,7 @@ def data
             vernierUserMetadata: profile.meta[:user_metadata],
             extra: [
               label: "User-Supplied Metadata",
-              entries: profile.meta[:user_metadata]&.map do |k, v|
+              entries: profile.meta[:user_metadata].map do |k, v|
                 {
                   label: k,
                   format: "string",


Test patch:
diff --git a/test/test_time_collector.rb b/test/test_time_collector.rb
index d83360d2..0eaabf1b 100644
--- a/test/test_time_collector.rb
+++ b/test/test_time_collector.rb
@@ -275,7 +275,8 @@ def test_includes_options_in_result_meta
     result = Vernier.profile(
       out: output_file,
       interval: SAMPLE_SCALE_INTERVAL,
-      allocation_interval: SAMPLE_SCALE_ALLOCATIONS
+      allocation_interval: SAMPLE_SCALE_ALLOCATIONS,
+      metadata: { key1: "val1", key2: "val2" }
     ) { }
 
     assert_equal :wall, result.meta[:mode]
@@ -283,6 +284,7 @@ def test_includes_options_in_result_meta
     assert_equal SAMPLE_SCALE_INTERVAL, result.meta[:interval]
     assert_equal SAMPLE_SCALE_ALLOCATIONS, result.meta[:allocation_interval]
     assert_equal false, result.meta[:gc]
+    assert_equal ({ key1: "val1", key2: "val2" }), result.meta[:user_metadata]
   end
 
   def test_switching_fibers