pyroscope_beta 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "rbspy"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "rbspy"
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ rutie = "0.8.3"
12
+ pyroscope = {version = "0.5" }
13
+ pyroscope_rbspy = { version = "0.2" }
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class RakeCargoHelper
6
+ attr_reader :gemname
7
+
8
+ def initialize(gemname=File.basename(__dir__))
9
+ @gemname = gemname
10
+ end
11
+
12
+ def self.command?(name)
13
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
14
+ ENV["PATH"].split(File::PATH_SEPARATOR).any? do |path|
15
+ exts.any? do |ext|
16
+ exe = File.join(path, "#{name}#{ext}")
17
+ File.executable?(exe) && !File.directory?(exe)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.rust_toolchain
23
+ str = `rustc --version --verbose`
24
+ info = str.lines.map {|l| l.chomp.split(/:\s+/, 2)}.drop(1).to_h
25
+ info["host"]
26
+ end
27
+
28
+ def self.cargo_target_dir
29
+ return @cargo_target_dir if defined? @cargo_target_dir
30
+
31
+ str = `cargo metadata --format-version 1 --offline --no-deps --quiet`
32
+ begin
33
+ require "json"
34
+ dir = JSON.parse(str)["target_directory"]
35
+ rescue LoadError # json is usually part of the stdlib, but just in case
36
+ /"target_directory"\s*:\s*"(?<dir>[^"]*)"/ =~ str
37
+ end
38
+ @cargo_target_dir = dir || "target"
39
+ end
40
+
41
+ def self.flags
42
+ cc_flags = Shellwords.split(RbConfig.expand(RbConfig::MAKEFILE_CONFIG["CC"].dup))
43
+
44
+ ["-C", "linker=#{cc_flags.shift}",
45
+ *cc_flags.flat_map {|a| ["-C", "link-arg=#{a}"] },
46
+ "-L", "native=#{RbConfig::CONFIG["libdir"]}",
47
+ *dld_flags,
48
+ *platform_flags]
49
+ end
50
+
51
+ def self.dld_flags
52
+ Shellwords.split(RbConfig::CONFIG["DLDFLAGS"]).flat_map do |arg|
53
+ arg = arg.gsub(/\$\((\w+)\)/) do
54
+ $1 == "DEFFILE" ? nil : RbConfig::CONFIG[name]
55
+ end.strip
56
+ next [] if arg.empty?
57
+
58
+ transform_flag(arg)
59
+ end
60
+ end
61
+
62
+ def self.platform_flags
63
+ return unless RbConfig::CONFIG["target_os"] =~ /mingw/i
64
+
65
+ [*Shellwords.split(RbConfig::CONFIG["LIBRUBYARG"]).flat_map {|arg| transform_flag(arg)},
66
+ "-C", "link-arg=-Wl,--dynamicbase",
67
+ "-C", "link-arg=-Wl,--disable-auto-image-base",
68
+ "-C", "link-arg=-static-libgcc"]
69
+ end
70
+
71
+ def self.transform_flag(arg)
72
+ k, v = arg.split(/(?<=..)/, 2)
73
+ case k
74
+ when "-L"
75
+ [k, "native=#{v}"]
76
+ when "-l"
77
+ [k, v]
78
+ when "-F"
79
+ ["-l", "framework=#{v}"]
80
+ else
81
+ ["-C", "link_arg=#{k}#{v}"]
82
+ end
83
+ end
84
+
85
+ def install_dir
86
+ File.expand_path(File.join("..", "..", "lib", gemname), __dir__)
87
+ end
88
+
89
+ def rust_name
90
+ prefix = "lib" unless Gem.win_platform?
91
+ suffix = if RbConfig::CONFIG["target_os"] =~ /darwin/i
92
+ ".dylib"
93
+ elsif Gem.win_platform?
94
+ ".dll"
95
+ else
96
+ ".so"
97
+ end
98
+ "#{prefix}#{gemname}#{suffix}"
99
+ end
100
+
101
+ def ruby_name
102
+ "#{gemname}.#{RbConfig::CONFIG["DLEXT"]}"
103
+ end
104
+
105
+ end
106
+
107
+ task default: [:install, :clean]
108
+
109
+ desc "set dev mode for subsequent task, run like `rake dev install`"
110
+ task :dev do
111
+ @dev = true
112
+ end
113
+
114
+ desc "build gem native extension and copy to lib"
115
+ task install: [:cd, :build] do
116
+ helper = RakeCargoHelper.new
117
+ profile_dir = @dev ? "debug" : "release"
118
+ source = File.join(RakeCargoHelper.cargo_target_dir, profile_dir, helper.rust_name)
119
+ dest = File.join(helper.install_dir, helper.ruby_name)
120
+ mkdir_p(helper.install_dir)
121
+ rm(dest) if File.exist?(dest)
122
+ cp(source, dest)
123
+ end
124
+
125
+ desc "build gem native extension"
126
+ task build: [:cargo, :cd] do
127
+ sh "cargo", "rustc", *(["--locked", "--release"] unless @dev), "--", *RakeCargoHelper.flags
128
+ end
129
+
130
+ desc "clean up release build artifacts"
131
+ task clean: [:cargo, :cd] do
132
+ sh "cargo clean --release"
133
+ end
134
+
135
+ desc "clean up build artifacts"
136
+ task clobber: [:cargo, :cd] do
137
+ sh "cargo clean"
138
+ end
139
+
140
+ desc "check for cargo"
141
+ task :cargo do
142
+ raise <<-MSG unless RakeCargoHelper.command?("cargo")
143
+ This gem requires a Rust compiler and the `cargo' build tool to build the
144
+ gem's native extension. See https://www.rust-lang.org/tools/install for
145
+ how to install Rust. `cargo' is usually part of the Rust installation.
146
+ MSG
147
+
148
+ raise <<-MSG if Gem.win_platform? && RakeCargoHelper.rust_toolchain !~ /gnu/
149
+ Found Rust toolchain `#{RakeCargoHelper.rust_toolchain}' but the gem native
150
+ extension requires the gnu toolchain on Windows.
151
+ MSG
152
+ end
153
+
154
+ # ensure task is running in the right dir
155
+ task :cd do
156
+ cd(__dir__) unless __dir__ == pwd
157
+ end
@@ -0,0 +1,11 @@
1
+ require 'mkmf'
2
+ require 'rake'
3
+
4
+ create_makefile('rbspy')
5
+
6
+ app = Rake.application
7
+ app.init
8
+ app.add_import 'Rakefile'
9
+ app.load_rakefile
10
+
11
+ app['default'].invoke
@@ -0,0 +1,156 @@
1
+ use pyroscope::backend::Tag;
2
+ use pyroscope::PyroscopeAgent;
3
+ use pyroscope_rbspy::{rbspy_backend, RbspyConfig};
4
+ use std::ffi::CStr;
5
+ use std::mem::MaybeUninit;
6
+ use std::os::raw::c_char;
7
+ use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
8
+ use std::sync::{Mutex, Once};
9
+
10
+ pub enum Signal {
11
+ Kill,
12
+ AddTag(u64, String, String),
13
+ RemoveTag(u64, String, String),
14
+ }
15
+
16
+ pub struct SignalPass {
17
+ inner_sender: Mutex<SyncSender<Signal>>,
18
+ inner_receiver: Mutex<Receiver<Signal>>,
19
+ }
20
+
21
+ fn signalpass() -> &'static SignalPass {
22
+ static mut SIGNAL_PASS: MaybeUninit<SignalPass> = MaybeUninit::uninit();
23
+ static ONCE: Once = Once::new();
24
+
25
+ ONCE.call_once(|| unsafe {
26
+ let (sender, receiver) = sync_channel(1);
27
+ let singleton = SignalPass {
28
+ inner_sender: Mutex::new(sender),
29
+ inner_receiver: Mutex::new(receiver),
30
+ };
31
+ SIGNAL_PASS = MaybeUninit::new(singleton);
32
+ });
33
+
34
+ unsafe { SIGNAL_PASS.assume_init_ref() }
35
+ }
36
+
37
+ #[link(name = "pyroscope_ffi", vers = "0.1")]
38
+ #[no_mangle]
39
+ pub fn initialize_agent(
40
+ application_name: *const c_char, server_address: *const c_char, sample_rate: u32,
41
+ detect_subprocesses: bool, tags: *const c_char,
42
+ ) -> bool {
43
+ let application_name = unsafe { CStr::from_ptr(application_name) }
44
+ .to_str()
45
+ .unwrap()
46
+ .to_string();
47
+ let server_address = unsafe { CStr::from_ptr(server_address) }
48
+ .to_str()
49
+ .unwrap()
50
+ .to_string();
51
+ let tags_string = unsafe { CStr::from_ptr(tags) }
52
+ .to_str()
53
+ .unwrap()
54
+ .to_string();
55
+
56
+ let pid = std::process::id();
57
+
58
+ let s = signalpass();
59
+
60
+ std::thread::spawn(move || {
61
+ let rbspy_config = RbspyConfig::new(pid.try_into().unwrap())
62
+ .sample_rate(sample_rate)
63
+ .lock_process(false)
64
+ .with_subprocesses(detect_subprocesses);
65
+
66
+ let tags_ref = tags_string.as_str();
67
+ let tags = string_to_tags(tags_ref);
68
+ let rbspy = rbspy_backend(rbspy_config);
69
+ let agent = PyroscopeAgent::builder(server_address, application_name)
70
+ .backend(rbspy)
71
+ .tags(tags)
72
+ .build()
73
+ .unwrap();
74
+
75
+ let agent_running = agent.start().unwrap();
76
+
77
+ while let Ok(signal) = s.inner_receiver.lock().unwrap().recv() {
78
+ match signal {
79
+ Signal::Kill => {
80
+ agent_running.stop().unwrap();
81
+ break;
82
+ }
83
+ Signal::AddTag(thread_id, key, value) => {
84
+ let tag = Tag::new(key, value);
85
+ agent_running.add_thread_tag(thread_id, tag).unwrap();
86
+ }
87
+ Signal::RemoveTag(thread_id, key, value) => {
88
+ let tag = Tag::new(key, value);
89
+ agent_running.remove_thread_tag(thread_id, tag).unwrap();
90
+ }
91
+ }
92
+ }
93
+ });
94
+
95
+ true
96
+ }
97
+
98
+ #[link(name = "pyroscope_ffi", vers = "0.1")]
99
+ #[no_mangle]
100
+ pub fn drop_agent() -> bool {
101
+ let s = signalpass();
102
+ s.inner_sender.lock().unwrap().send(Signal::Kill).unwrap();
103
+ true
104
+ }
105
+
106
+ #[link(name = "pyroscope_ffi", vers = "0.1")]
107
+ #[no_mangle]
108
+ pub fn add_tag(thread_id: u64, key: *const c_char, value: *const c_char) -> bool {
109
+ let s = signalpass();
110
+ let key = unsafe { CStr::from_ptr(key) }.to_str().unwrap().to_owned();
111
+ let value = unsafe { CStr::from_ptr(value) }
112
+ .to_str()
113
+ .unwrap()
114
+ .to_owned();
115
+ s.inner_sender
116
+ .lock()
117
+ .unwrap()
118
+ .send(Signal::AddTag(thread_id, key, value))
119
+ .unwrap();
120
+ true
121
+ }
122
+
123
+ #[link(name = "pyroscope_ffi", vers = "0.1")]
124
+ #[no_mangle]
125
+ pub fn remove_tag(thread_id: u64, key: *const c_char, value: *const c_char) -> bool {
126
+ let s = signalpass();
127
+ let key = unsafe { CStr::from_ptr(key) }.to_str().unwrap().to_owned();
128
+ let value = unsafe { CStr::from_ptr(value) }
129
+ .to_str()
130
+ .unwrap()
131
+ .to_owned();
132
+ s.inner_sender
133
+ .lock()
134
+ .unwrap()
135
+ .send(Signal::RemoveTag(thread_id, key, value))
136
+ .unwrap();
137
+ true
138
+ }
139
+
140
+ // Convert a string of tags to a Vec<(&str, &str)>
141
+ fn string_to_tags<'a>(tags: &'a str) -> Vec<(&'a str, &'a str)> {
142
+ let mut tags_vec = Vec::new();
143
+ // check if string is empty
144
+ if tags.is_empty() {
145
+ return tags_vec;
146
+ }
147
+
148
+ for tag in tags.split(',') {
149
+ let mut tag_split = tag.split('=');
150
+ let key = tag_split.next().unwrap();
151
+ let value = tag_split.next().unwrap();
152
+ tags_vec.push((key, value));
153
+ }
154
+
155
+ tags_vec
156
+ }
@@ -0,0 +1,16 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 3
4
+
5
+ [[package]]
6
+ name = "libc"
7
+ version = "0.2.125"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
10
+
11
+ [[package]]
12
+ name = "thread_id"
13
+ version = "0.1.0"
14
+ dependencies = [
15
+ "libc",
16
+ ]
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "thread_id"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "thread_id"
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ libc = "*"
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class RakeCargoHelper
6
+ attr_reader :gemname
7
+
8
+ def initialize(gemname=File.basename(__dir__))
9
+ @gemname = gemname
10
+ end
11
+
12
+ def self.command?(name)
13
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
14
+ ENV["PATH"].split(File::PATH_SEPARATOR).any? do |path|
15
+ exts.any? do |ext|
16
+ exe = File.join(path, "#{name}#{ext}")
17
+ File.executable?(exe) && !File.directory?(exe)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.rust_toolchain
23
+ str = `rustc --version --verbose`
24
+ info = str.lines.map {|l| l.chomp.split(/:\s+/, 2)}.drop(1).to_h
25
+ info["host"]
26
+ end
27
+
28
+ def self.cargo_target_dir
29
+ return @cargo_target_dir if defined? @cargo_target_dir
30
+
31
+ str = `cargo metadata --format-version 1 --offline --no-deps --quiet`
32
+ begin
33
+ require "json"
34
+ dir = JSON.parse(str)["target_directory"]
35
+ rescue LoadError # json is usually part of the stdlib, but just in case
36
+ /"target_directory"\s*:\s*"(?<dir>[^"]*)"/ =~ str
37
+ end
38
+ @cargo_target_dir = dir || "target"
39
+ end
40
+
41
+ def self.flags
42
+ cc_flags = Shellwords.split(RbConfig.expand(RbConfig::MAKEFILE_CONFIG["CC"].dup))
43
+
44
+ ["-C", "linker=#{cc_flags.shift}",
45
+ *cc_flags.flat_map {|a| ["-C", "link-arg=#{a}"] },
46
+ "-L", "native=#{RbConfig::CONFIG["libdir"]}",
47
+ *dld_flags,
48
+ *platform_flags]
49
+ end
50
+
51
+ def self.dld_flags
52
+ Shellwords.split(RbConfig::CONFIG["DLDFLAGS"]).flat_map do |arg|
53
+ arg = arg.gsub(/\$\((\w+)\)/) do
54
+ $1 == "DEFFILE" ? nil : RbConfig::CONFIG[name]
55
+ end.strip
56
+ next [] if arg.empty?
57
+
58
+ transform_flag(arg)
59
+ end
60
+ end
61
+
62
+ def self.platform_flags
63
+ return unless RbConfig::CONFIG["target_os"] =~ /mingw/i
64
+
65
+ [*Shellwords.split(RbConfig::CONFIG["LIBRUBYARG"]).flat_map {|arg| transform_flag(arg)},
66
+ "-C", "link-arg=-Wl,--dynamicbase",
67
+ "-C", "link-arg=-Wl,--disable-auto-image-base",
68
+ "-C", "link-arg=-static-libgcc"]
69
+ end
70
+
71
+ def self.transform_flag(arg)
72
+ k, v = arg.split(/(?<=..)/, 2)
73
+ case k
74
+ when "-L"
75
+ [k, "native=#{v}"]
76
+ when "-l"
77
+ [k, v]
78
+ when "-F"
79
+ ["-l", "framework=#{v}"]
80
+ else
81
+ ["-C", "link_arg=#{k}#{v}"]
82
+ end
83
+ end
84
+
85
+ def install_dir
86
+ File.expand_path(File.join("..", "..", "lib", gemname), __dir__)
87
+ end
88
+
89
+ def rust_name
90
+ prefix = "lib" unless Gem.win_platform?
91
+ suffix = if RbConfig::CONFIG["target_os"] =~ /darwin/i
92
+ ".dylib"
93
+ elsif Gem.win_platform?
94
+ ".dll"
95
+ else
96
+ ".so"
97
+ end
98
+ "#{prefix}#{gemname}#{suffix}"
99
+ end
100
+
101
+ def ruby_name
102
+ "#{gemname}.#{RbConfig::CONFIG["DLEXT"]}"
103
+ end
104
+
105
+ end
106
+
107
+ task default: [:install, :clean]
108
+
109
+ desc "set dev mode for subsequent task, run like `rake dev install`"
110
+ task :dev do
111
+ @dev = true
112
+ end
113
+
114
+ desc "build gem native extension and copy to lib"
115
+ task install: [:cd, :build] do
116
+ helper = RakeCargoHelper.new
117
+ profile_dir = @dev ? "debug" : "release"
118
+ source = File.join(RakeCargoHelper.cargo_target_dir, profile_dir, helper.rust_name)
119
+ dest = File.join(helper.install_dir, helper.ruby_name)
120
+ mkdir_p(helper.install_dir)
121
+ rm(dest) if File.exist?(dest)
122
+ cp(source, dest)
123
+ end
124
+
125
+ desc "build gem native extension"
126
+ task build: [:cargo, :cd] do
127
+ sh "cargo", "rustc", *(["--locked", "--release"] unless @dev), "--", *RakeCargoHelper.flags
128
+ end
129
+
130
+ desc "clean up release build artifacts"
131
+ task clean: [:cargo, :cd] do
132
+ sh "cargo clean --release"
133
+ end
134
+
135
+ desc "clean up build artifacts"
136
+ task clobber: [:cargo, :cd] do
137
+ sh "cargo clean"
138
+ end
139
+
140
+ desc "check for cargo"
141
+ task :cargo do
142
+ raise <<-MSG unless RakeCargoHelper.command?("cargo")
143
+ This gem requires a Rust compiler and the `cargo' build tool to build the
144
+ gem's native extension. See https://www.rust-lang.org/tools/install for
145
+ how to install Rust. `cargo' is usually part of the Rust installation.
146
+ MSG
147
+
148
+ raise <<-MSG if Gem.win_platform? && RakeCargoHelper.rust_toolchain !~ /gnu/
149
+ Found Rust toolchain `#{RakeCargoHelper.rust_toolchain}' but the gem native
150
+ extension requires the gnu toolchain on Windows.
151
+ MSG
152
+ end
153
+
154
+ # ensure task is running in the right dir
155
+ task :cd do
156
+ cd(__dir__) unless __dir__ == pwd
157
+ end
@@ -0,0 +1,11 @@
1
+ require 'mkmf'
2
+ require 'rake'
3
+
4
+ create_makefile('thread_id')
5
+
6
+ app = Rake.application
7
+ app.init
8
+ app.add_import 'Rakefile'
9
+ app.load_rakefile
10
+
11
+ app['default'].invoke
@@ -0,0 +1,4 @@
1
+ #[no_mangle]
2
+ pub extern "C" fn thread_id() -> u64 {
3
+ unsafe { libc::pthread_self() as u64 }
4
+ }
@@ -0,0 +1,3 @@
1
+ module Pyroscope
2
+ VERSION = '0.1.1'.freeze
3
+ end
@@ -0,0 +1,87 @@
1
+ require 'ffi'
2
+ require 'fiddle'
3
+
4
+ $libm = Fiddle.dlopen(File.expand_path(File.dirname(__FILE__)) + '/thread_id/thread_id.so')
5
+
6
+
7
+ module Rust
8
+ extend FFI::Library
9
+ ffi_lib File.expand_path(File.dirname(__FILE__)) + '/rbspy/rbspy.' + FFI::Platform::LIBSUFFIX
10
+ attach_function :initialize_agent, [:string, :string, :int, :bool, :string], :bool
11
+ attach_function :add_tag, [:uint64, :string, :string], :bool
12
+ attach_function :remove_tag, [:uint64, :string, :string], :bool
13
+ attach_function :drop_agent, [], :bool
14
+ end
15
+
16
+ module Pyroscope
17
+ Config = Struct.new(:application_name, :server_address, :sample_rate, :detect_subprocesses, :log_level, :tags) do
18
+ def initialize(*)
19
+ super
20
+ self.application_name ||= ''
21
+ self.server_address ||= 'http://localhost:4040'
22
+ self.sample_rate ||= 100
23
+ self.detect_subprocesses ||= true
24
+ self.log_level ||= 'info'
25
+ self.tags ||= []
26
+ end
27
+ end
28
+
29
+ class << self
30
+ def configure
31
+ @config = Config.new
32
+
33
+ # Pass config to the block
34
+ yield @config
35
+
36
+ Rust.initialize_agent(
37
+ @config.application_name,
38
+ @config.server_address,
39
+ @config.sample_rate,
40
+ @config.detect_subprocesses,
41
+ tags_to_string(@config.tags)
42
+ )
43
+
44
+ puts @config
45
+ end
46
+
47
+ def tag_wrapper(tags)
48
+ add_tags(tags)
49
+ begin
50
+ yield
51
+ ensure
52
+ remove_tags(tags)
53
+ end
54
+ end
55
+
56
+ def drop
57
+ Rust.drop_agent
58
+ end
59
+ end
60
+ end
61
+
62
+ # convert tags object to string
63
+ def tags_to_string(tags)
64
+ tags.map { |k, v| "#{k}=#{v}" }.join(',')
65
+ end
66
+
67
+ # get thread id
68
+ def thread_id
69
+ thread_id = Fiddle::Function.new($libm['thread_id'], [], Fiddle::TYPE_INT64_T)
70
+ thread_id.call
71
+ end
72
+
73
+ # add tags
74
+ def add_tags(tags)
75
+ tags.each do |tag_name, tag_value|
76
+ thread_id = thread_id()
77
+ Rust.add_tag(thread_id, tag_name.to_s, tag_value.to_s)
78
+ end
79
+ end
80
+
81
+ # remove tags
82
+ def remove_tags(tags)
83
+ tags.each do |tag_name, tag_value|
84
+ thread_id = thread_id()
85
+ Rust.remove_tag(thread_id, tag_name.to_s, tag_value.to_s)
86
+ end
87
+ end