railst 0.2.0
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.
- checksums.yaml +7 -0
- data/README.md +29 -0
- data/exe/railst +9 -0
- data/lib/railst/cli.rb +742 -0
- data/lib/railst/environment.rb +65 -0
- data/lib/railst/templates.rb +503 -0
- data/lib/railst/terminal.rb +80 -0
- data/lib/railst/version.rb +5 -0
- data/lib/railst.rb +10 -0
- metadata +50 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railst
|
|
4
|
+
module Environment
|
|
5
|
+
def self.toolchains_env
|
|
6
|
+
env = ENV.to_h
|
|
7
|
+
|
|
8
|
+
# Dynamically detect gem user bin, cargo bin, and local bin
|
|
9
|
+
gem_bin = begin
|
|
10
|
+
user_dir = `ruby -r rubygems -e "puts Gem.user_dir" 2>/dev/null`.strip
|
|
11
|
+
File.join(user_dir, "bin") unless user_dir.empty?
|
|
12
|
+
rescue StandardError
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
cargo_bin = File.expand_path("~/.cargo/bin")
|
|
17
|
+
local_bin = File.expand_path("~/.local/bin")
|
|
18
|
+
|
|
19
|
+
current_paths = env.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
20
|
+
[ cargo_bin, local_bin, gem_bin ].compact.each do |p|
|
|
21
|
+
current_paths.unshift(p) if File.directory?(p) && !current_paths.include?(p)
|
|
22
|
+
end
|
|
23
|
+
env["PATH"] = current_paths.join(File::PATH_SEPARATOR)
|
|
24
|
+
|
|
25
|
+
# Automatic LLVM / Clang detection for bindgen & rb-sys
|
|
26
|
+
unless env["LIBCLANG_PATH"]
|
|
27
|
+
candidate_lib_paths = [
|
|
28
|
+
File.expand_path("~/.local/lib"),
|
|
29
|
+
"/usr/lib/llvm-23/lib",
|
|
30
|
+
"/usr/lib/llvm-22/lib",
|
|
31
|
+
"/usr/lib/llvm-21/lib",
|
|
32
|
+
"/usr/lib/llvm-20/lib",
|
|
33
|
+
"/usr/lib/llvm-19/lib",
|
|
34
|
+
"/usr/lib/llvm-18/lib",
|
|
35
|
+
"/usr/lib",
|
|
36
|
+
"/usr/local/lib"
|
|
37
|
+
]
|
|
38
|
+
found_lib = candidate_lib_paths.find do |dir|
|
|
39
|
+
File.directory?(dir) && !Dir[File.join(dir, "libclang.so*")].empty?
|
|
40
|
+
end
|
|
41
|
+
env["LIBCLANG_PATH"] = found_lib if found_lib
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless env["BINDGEN_EXTRA_CLANG_ARGS"]
|
|
45
|
+
candidate_include_paths = [
|
|
46
|
+
File.expand_path("~/.local/lib/clang/23/include"),
|
|
47
|
+
"/usr/lib/clang/23/include",
|
|
48
|
+
"/usr/lib/clang/22/include",
|
|
49
|
+
"/usr/lib/clang/21/include",
|
|
50
|
+
"/usr/lib/clang/20/include",
|
|
51
|
+
"/usr/lib/clang/19/include",
|
|
52
|
+
"/usr/lib/clang/18/include"
|
|
53
|
+
]
|
|
54
|
+
found_inc = candidate_include_paths.find { |dir| File.directory?(dir) }
|
|
55
|
+
env["BINDGEN_EXTRA_CLANG_ARGS"] = "-I#{found_inc}" if found_inc
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Quiet down noisy glib / libvips system module warnings
|
|
59
|
+
env["VIPS_WARNING"] = "0"
|
|
60
|
+
env["G_MESSAGES_DEBUG"] = "none"
|
|
61
|
+
|
|
62
|
+
env
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Railst
|
|
7
|
+
module Templates
|
|
8
|
+
def self.inject_into(target_dir, source_project_root = nil)
|
|
9
|
+
target_path = Pathname.new(target_dir)
|
|
10
|
+
|
|
11
|
+
# 1. Check for template in monorepo packages/template or source root
|
|
12
|
+
monorepo_template = nil
|
|
13
|
+
if source_project_root
|
|
14
|
+
candidates = [
|
|
15
|
+
source_project_root.join("packages/template"),
|
|
16
|
+
source_project_root
|
|
17
|
+
]
|
|
18
|
+
monorepo_template = candidates.find { |c| c.join("ext/rust_backend").exist? }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if monorepo_template
|
|
22
|
+
# Copy backend & support files from existing monorepo template
|
|
23
|
+
copy_tree(monorepo_template.join("ext/rust_backend"), target_path.join("ext/rust_backend"))
|
|
24
|
+
copy_file_safe(monorepo_template.join("app/models/concerns/rust_backed.rb"), target_path.join("app/models/concerns/rust_backed.rb"))
|
|
25
|
+
copy_file_safe(monorepo_template.join("config/initializers/rust_backend.rb"), target_path.join("config/initializers/rust_backend.rb"))
|
|
26
|
+
copy_file_safe(monorepo_template.join("lib/rust_backend.rb"), target_path.join("lib/rust_backend.rb"))
|
|
27
|
+
copy_file_safe(monorepo_template.join("lib/tasks/rust.rake"), target_path.join("lib/tasks/rust.rake"))
|
|
28
|
+
copy_tree(monorepo_template.join("lib/generators/rust"), target_path.join("lib/generators/rust"))
|
|
29
|
+
else
|
|
30
|
+
# Self-contained embedded template generation
|
|
31
|
+
write_embedded_templates(target_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Update config/application.rb for autoloading
|
|
35
|
+
app_rb = target_path.join("config/application.rb")
|
|
36
|
+
if app_rb.exist?
|
|
37
|
+
code = app_rb.read
|
|
38
|
+
autoload_line = "config.autoload_lib(ignore: %w[assets tasks generators rust_backend])"
|
|
39
|
+
unless code.include?(autoload_line)
|
|
40
|
+
code.sub!(/class Application < Rails::Application\n/) do |m|
|
|
41
|
+
"#{m} #{autoload_line}\n"
|
|
42
|
+
end
|
|
43
|
+
app_rb.write(code)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.copy_tree(src, dst)
|
|
49
|
+
return unless src.exist?
|
|
50
|
+
|
|
51
|
+
FileUtils.mkdir_p(dst)
|
|
52
|
+
FileUtils.cp_r(File.join(src, "."), dst)
|
|
53
|
+
FileUtils.rm_rf(File.join(dst, "target"))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.copy_file_safe(src, dst)
|
|
57
|
+
return unless src.exist?
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(dst.parent)
|
|
60
|
+
FileUtils.cp(src, dst)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.write_embedded_templates(target_path)
|
|
64
|
+
files = {
|
|
65
|
+
"ext/rust_backend/Cargo.toml" => cargo_toml_content,
|
|
66
|
+
"ext/rust_backend/extconf.rb" => extconf_rb_content,
|
|
67
|
+
"ext/rust_backend/src/lib.rs" => lib_rs_content,
|
|
68
|
+
"ext/rust_backend/src/gvl.rs" => gvl_rs_content,
|
|
69
|
+
"ext/rust_backend/src/dynamic_hash.rs" => dynamic_hash_rs_content,
|
|
70
|
+
"ext/rust_backend/src/data_processor.rs" => data_processor_rs_content,
|
|
71
|
+
"app/models/concerns/rust_backed.rb" => rust_backed_rb_content,
|
|
72
|
+
"config/initializers/rust_backend.rb" => initializer_content,
|
|
73
|
+
"lib/rust_backend.rb" => loader_content,
|
|
74
|
+
"lib/tasks/rust.rake" => rake_task_content
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
files.each do |rel_path, content|
|
|
78
|
+
full_path = target_path.join(rel_path)
|
|
79
|
+
FileUtils.mkdir_p(full_path.parent)
|
|
80
|
+
full_path.write(content)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.cargo_toml_content
|
|
85
|
+
<<~TOML
|
|
86
|
+
[package]
|
|
87
|
+
name = "rust_backend"
|
|
88
|
+
version = "0.1.0"
|
|
89
|
+
edition = "2021"
|
|
90
|
+
|
|
91
|
+
[lib]
|
|
92
|
+
crate-type = ["cdylib"]
|
|
93
|
+
|
|
94
|
+
[dependencies]
|
|
95
|
+
magnus = "0.9"
|
|
96
|
+
rb-sys = "0.9"
|
|
97
|
+
rayon = "1.10"
|
|
98
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
99
|
+
serde_json = "1.0"
|
|
100
|
+
TOML
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.extconf_rb_content
|
|
104
|
+
<<~RUBY
|
|
105
|
+
# frozen_string_literal: true
|
|
106
|
+
|
|
107
|
+
require "mkmf"
|
|
108
|
+
require "rb_sys/mkmf"
|
|
109
|
+
|
|
110
|
+
create_rust_makefile("rust_backend/rust_backend")
|
|
111
|
+
RUBY
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.lib_rs_content
|
|
115
|
+
<<~RUST
|
|
116
|
+
use magnus::{Error, Ruby};
|
|
117
|
+
|
|
118
|
+
pub mod dynamic_hash;
|
|
119
|
+
pub mod data_processor;
|
|
120
|
+
pub mod gvl;
|
|
121
|
+
|
|
122
|
+
#[magnus::init]
|
|
123
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
124
|
+
let module = ruby.define_module("RustBackend")?;
|
|
125
|
+
|
|
126
|
+
module.define_singleton_method(
|
|
127
|
+
"process_batch",
|
|
128
|
+
magnus::function!(data_processor::process_batch, 2),
|
|
129
|
+
)?;
|
|
130
|
+
|
|
131
|
+
module.define_singleton_method(
|
|
132
|
+
"aggregate_metrics",
|
|
133
|
+
magnus::function!(data_processor::aggregate_metrics, 2),
|
|
134
|
+
)?;
|
|
135
|
+
|
|
136
|
+
module.define_singleton_method(
|
|
137
|
+
"transform_record",
|
|
138
|
+
magnus::function!(data_processor::transform_record, 2),
|
|
139
|
+
)?;
|
|
140
|
+
|
|
141
|
+
Ok(())
|
|
142
|
+
}
|
|
143
|
+
RUST
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.gvl_rs_content
|
|
147
|
+
<<~RUST
|
|
148
|
+
use magnus::rb_sys::rb_thread_call_without_gvl;
|
|
149
|
+
use std::os::raw::c_void;
|
|
150
|
+
|
|
151
|
+
extern "C" fn nogvl_trampoline<F, R>(ptr: *mut c_void) -> *mut c_void
|
|
152
|
+
where
|
|
153
|
+
F: FnOnce() -> R,
|
|
154
|
+
{
|
|
155
|
+
let f = unsafe { Box::from_raw(ptr as *mut F) };
|
|
156
|
+
let result = f();
|
|
157
|
+
Box::into_raw(Box::new(result)) as *mut c_void
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub fn call_without_gvl<F, R>(f: F) -> R
|
|
161
|
+
where
|
|
162
|
+
F: FnOnce() -> R + Send,
|
|
163
|
+
R: Send,
|
|
164
|
+
{
|
|
165
|
+
let boxed_f = Box::new(f);
|
|
166
|
+
let ptr = Box::into_raw(boxed_f) as *mut c_void;
|
|
167
|
+
|
|
168
|
+
let res_ptr = unsafe {
|
|
169
|
+
rb_thread_call_without_gvl(
|
|
170
|
+
Some(nogvl_trampoline::<F, R>),
|
|
171
|
+
ptr,
|
|
172
|
+
None,
|
|
173
|
+
std::ptr::null_mut(),
|
|
174
|
+
)
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
let boxed_res = unsafe { Box::from_raw(res_ptr as *mut R) };
|
|
178
|
+
*boxed_res
|
|
179
|
+
}
|
|
180
|
+
RUST
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.dynamic_hash_rs_content
|
|
184
|
+
<<~RUST
|
|
185
|
+
use magnus::{value::ReprValue, Error, RArray, RHash, Ruby, Symbol, Value};
|
|
186
|
+
use serde_json::{Map, Number, Value as JsonValue};
|
|
187
|
+
|
|
188
|
+
pub fn rval_to_json(val: Value) -> JsonValue {
|
|
189
|
+
if val.is_nil() {
|
|
190
|
+
return JsonValue::Null;
|
|
191
|
+
}
|
|
192
|
+
if let Ok(b) = bool::try_convert(val) {
|
|
193
|
+
return JsonValue::Bool(b);
|
|
194
|
+
}
|
|
195
|
+
if let Ok(i) = i64::try_convert(val) {
|
|
196
|
+
return JsonValue::Number(Number::from(i));
|
|
197
|
+
}
|
|
198
|
+
if let Ok(f) = f64::try_convert(val) {
|
|
199
|
+
if let Some(n) = Number::from_f64(f) {
|
|
200
|
+
return JsonValue::Number(n);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if let Ok(s) = String::try_convert(val) {
|
|
204
|
+
return JsonValue::String(s);
|
|
205
|
+
}
|
|
206
|
+
if let Ok(sym) = Symbol::try_convert(val) {
|
|
207
|
+
if let Ok(s) = sym.name() {
|
|
208
|
+
return JsonValue::String(s.to_string());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if let Ok(hash) = RHash::try_convert(val) {
|
|
212
|
+
let mut map = Map::new();
|
|
213
|
+
let _ = hash.each(|k: Value, v: Value| {
|
|
214
|
+
let key_str = if let Ok(s) = String::try_convert(k) {
|
|
215
|
+
s
|
|
216
|
+
} else if let Ok(sym) = Symbol::try_convert(k) {
|
|
217
|
+
sym.name().map(|n| n.to_string()).unwrap_or_else(|_| k.to_string())
|
|
218
|
+
} else {
|
|
219
|
+
k.to_string()
|
|
220
|
+
};
|
|
221
|
+
map.insert(key_str, rval_to_json(v));
|
|
222
|
+
Ok(())
|
|
223
|
+
});
|
|
224
|
+
return JsonValue::Object(map);
|
|
225
|
+
}
|
|
226
|
+
if let Ok(arr) = RArray::try_convert(val) {
|
|
227
|
+
let mut vec = Vec::with_capacity(arr.len());
|
|
228
|
+
for item in arr.into_iter() {
|
|
229
|
+
vec.push(rval_to_json(item));
|
|
230
|
+
}
|
|
231
|
+
return JsonValue::Array(vec);
|
|
232
|
+
}
|
|
233
|
+
JsonValue::String(val.to_string())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
pub fn json_to_rval(ruby: &Ruby, val: &JsonValue) -> Result<Value, Error> {
|
|
237
|
+
match val {
|
|
238
|
+
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
239
|
+
JsonValue::Bool(b) => Ok(ruby.into_value(*b)),
|
|
240
|
+
JsonValue::Number(n) => {
|
|
241
|
+
if let Some(i) = n.as_i64() {
|
|
242
|
+
Ok(ruby.into_value(i))
|
|
243
|
+
} else if let Some(f) = n.as_f64() {
|
|
244
|
+
Ok(ruby.into_value(f))
|
|
245
|
+
} else {
|
|
246
|
+
Ok(ruby.into_value(n.to_string()))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
JsonValue::String(s) => Ok(ruby.into_value(s.as_str())),
|
|
250
|
+
JsonValue::Array(arr) => {
|
|
251
|
+
let rarr = ruby.ary_new_capa(arr.len());
|
|
252
|
+
for item in arr {
|
|
253
|
+
rarr.push(json_to_rval(ruby, item)?)?;
|
|
254
|
+
}
|
|
255
|
+
Ok(rarr.as_value())
|
|
256
|
+
}
|
|
257
|
+
JsonValue::Object(map) => {
|
|
258
|
+
let rhash = ruby.hash_new();
|
|
259
|
+
for (k, v) in map {
|
|
260
|
+
let rkey = ruby.into_value(k.as_str());
|
|
261
|
+
let rval = json_to_rval(ruby, v)?;
|
|
262
|
+
rhash.aset(rkey, rval)?;
|
|
263
|
+
}
|
|
264
|
+
Ok(rhash.as_value())
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
RUST
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.data_processor_rs_content
|
|
272
|
+
<<~RUST
|
|
273
|
+
use crate::dynamic_hash::{json_to_rval, rval_to_json};
|
|
274
|
+
use crate::gvl::call_without_gvl;
|
|
275
|
+
use magnus::{value::ReprValue, Error, RArray, RHash, Ruby, Value};
|
|
276
|
+
use rayon::prelude::*;
|
|
277
|
+
use serde_json::{json, Value as JsonValue};
|
|
278
|
+
|
|
279
|
+
pub fn transform_record(ruby: &Ruby, record: RHash, config: RHash) -> Result<Value, Error> {
|
|
280
|
+
let mut raw_record = rval_to_json(record.as_value());
|
|
281
|
+
let raw_config = rval_to_json(config.as_value());
|
|
282
|
+
|
|
283
|
+
let field = raw_config.get("compute_field").and_then(|v| v.as_str()).unwrap_or("value").to_string();
|
|
284
|
+
let mult = raw_config.get("multiplier").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
|
285
|
+
|
|
286
|
+
if let JsonValue::Object(ref mut map) = raw_record {
|
|
287
|
+
let current_val = map.get(&field).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
288
|
+
map.insert(format!("{}_computed", field), json!(current_val * mult));
|
|
289
|
+
map.insert("processed_by".to_string(), json!("rust_magnus"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
json_to_rval(ruby, &raw_record)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
pub fn process_batch(ruby: &Ruby, records: RArray, config: RHash) -> Result<Value, Error> {
|
|
296
|
+
let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
|
|
297
|
+
let raw_config = rval_to_json(config.as_value());
|
|
298
|
+
|
|
299
|
+
let compute_field = raw_config
|
|
300
|
+
.get("compute_field")
|
|
301
|
+
.and_then(|v| v.as_str())
|
|
302
|
+
.unwrap_or("value")
|
|
303
|
+
.to_string();
|
|
304
|
+
|
|
305
|
+
let multiplier = raw_config
|
|
306
|
+
.get("multiplier")
|
|
307
|
+
.and_then(|v| v.as_f64())
|
|
308
|
+
.unwrap_or(1.0);
|
|
309
|
+
|
|
310
|
+
let offset = raw_config
|
|
311
|
+
.get("offset")
|
|
312
|
+
.and_then(|v| v.as_f64())
|
|
313
|
+
.unwrap_or(0.0);
|
|
314
|
+
|
|
315
|
+
let processed_records = call_without_gvl(move || {
|
|
316
|
+
raw_records
|
|
317
|
+
.into_par_iter()
|
|
318
|
+
.map(|mut rec| {
|
|
319
|
+
if let JsonValue::Object(ref mut map) = rec {
|
|
320
|
+
let current_val = map
|
|
321
|
+
.get(&compute_field)
|
|
322
|
+
.and_then(|v| {
|
|
323
|
+
v.as_f64().or_else(|| {
|
|
324
|
+
v.as_str().and_then(|s| s.parse::<f64>().ok())
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
.unwrap_or(0.0);
|
|
328
|
+
|
|
329
|
+
let computed = (current_val * multiplier) + offset;
|
|
330
|
+
map.insert(format!("{}_computed", compute_field), json!(computed));
|
|
331
|
+
map.insert("processed_by".to_string(), json!("rust_rayon_parallel"));
|
|
332
|
+
map.insert("gvl_released".to_string(), json!(true));
|
|
333
|
+
}
|
|
334
|
+
rec
|
|
335
|
+
})
|
|
336
|
+
.collect::<Vec<JsonValue>>()
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
let result_json = JsonValue::Array(processed_records);
|
|
340
|
+
json_to_rval(ruby, &result_json)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
pub fn aggregate_metrics(ruby: &Ruby, records: RArray, metric_field: String) -> Result<Value, Error> {
|
|
344
|
+
let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
|
|
345
|
+
let target_field = metric_field.clone();
|
|
346
|
+
|
|
347
|
+
let (count, sum, min, max) = call_without_gvl(move || {
|
|
348
|
+
let values: Vec<f64> = raw_records
|
|
349
|
+
.into_par_iter()
|
|
350
|
+
.filter_map(|rec| {
|
|
351
|
+
if let JsonValue::Object(map) = rec {
|
|
352
|
+
map.get(&target_field).and_then(|v| {
|
|
353
|
+
v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
None
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
.collect();
|
|
360
|
+
|
|
361
|
+
if values.is_empty() {
|
|
362
|
+
(0, 0.0, 0.0, 0.0)
|
|
363
|
+
} else {
|
|
364
|
+
let count = values.len();
|
|
365
|
+
let sum: f64 = values.par_iter().sum();
|
|
366
|
+
let min = values.par_iter().copied().reduce(|| f64::INFINITY, f64::min);
|
|
367
|
+
let max = values.par_iter().copied().reduce(|| f64::NEG_INFINITY, f64::max);
|
|
368
|
+
(count, sum, min, max)
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
let avg = if count > 0 { sum / count as f64 } else { 0.0 };
|
|
373
|
+
|
|
374
|
+
let summary = json!({
|
|
375
|
+
"field": metric_field,
|
|
376
|
+
"count": count,
|
|
377
|
+
"sum": sum,
|
|
378
|
+
"avg": avg,
|
|
379
|
+
"min": min,
|
|
380
|
+
"max": max,
|
|
381
|
+
"parallel_engine": "rayon"
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
json_to_rval(ruby, &summary)
|
|
385
|
+
}
|
|
386
|
+
RUST
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def self.rust_backed_rb_content
|
|
390
|
+
<<~RUBY
|
|
391
|
+
# frozen_string_literal: true
|
|
392
|
+
|
|
393
|
+
module RustBacked
|
|
394
|
+
extend ActiveSupport::Concern
|
|
395
|
+
|
|
396
|
+
included do
|
|
397
|
+
# ActiveSupport concern for models integrating with Magnus Rust extension
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def process_with_rust
|
|
401
|
+
RustBackend.transform_record(attributes, { "multiplier" => 1.0 })
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
class_methods do
|
|
405
|
+
# Batch process dynamic records across CPU cores with Rayon and GVL released
|
|
406
|
+
def rust_batch_process(scope_or_hashes, config = {})
|
|
407
|
+
raw = if scope_or_hashes.respond_to?(:map)
|
|
408
|
+
scope_or_hashes.map { |r| r.respond_to?(:attributes) ? r.attributes : r }
|
|
409
|
+
else
|
|
410
|
+
scope_or_hashes
|
|
411
|
+
end
|
|
412
|
+
RustBackend.process_batch(raw, config.stringify_keys)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# High speed parallel statistical aggregation across CPU cores (GVL released)
|
|
416
|
+
def rust_aggregate(scope_or_hashes, field)
|
|
417
|
+
raw = if scope_or_hashes.respond_to?(:map)
|
|
418
|
+
scope_or_hashes.map { |r| r.respond_to?(:attributes) ? r.attributes : r }
|
|
419
|
+
else
|
|
420
|
+
scope_or_hashes
|
|
421
|
+
end
|
|
422
|
+
RustBackend.aggregate_metrics(raw, field.to_s)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
RUBY
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def self.initializer_content
|
|
430
|
+
<<~RUBY
|
|
431
|
+
# frozen_string_literal: true
|
|
432
|
+
|
|
433
|
+
require "rust_backend"
|
|
434
|
+
|
|
435
|
+
Rails.logger.info "[RustBackend] Magnus native Rust backend loaded successfully."
|
|
436
|
+
RUBY
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def self.loader_content
|
|
440
|
+
<<~RUBY
|
|
441
|
+
# frozen_string_literal: true
|
|
442
|
+
|
|
443
|
+
module RustBackend
|
|
444
|
+
begin
|
|
445
|
+
require_relative "rust_backend/rust_backend"
|
|
446
|
+
rescue LoadError
|
|
447
|
+
require_relative "../ext/rust_backend/rust_backend"
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
RUBY
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def self.rake_task_content
|
|
454
|
+
<<~RUBY
|
|
455
|
+
# frozen_string_literal: true
|
|
456
|
+
|
|
457
|
+
def rust_build_env
|
|
458
|
+
cargo_path = File.expand_path("~/.cargo/bin")
|
|
459
|
+
env = {
|
|
460
|
+
"LIBCLANG_PATH" => ENV["LIBCLANG_PATH"] || File.expand_path("~/.local/lib"),
|
|
461
|
+
"BINDGEN_EXTRA_CLANG_ARGS" => ENV["BINDGEN_EXTRA_CLANG_ARGS"] || "-I\#{File.expand_path('~/.local/lib/clang/23/include')}"
|
|
462
|
+
}
|
|
463
|
+
env["PATH"] = "\#{cargo_path}:\#{ENV['PATH']}" unless ENV["PATH"].include?(cargo_path)
|
|
464
|
+
env
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
namespace :rust do
|
|
468
|
+
desc "Build the Magnus Rust backend extension"
|
|
469
|
+
task :build do
|
|
470
|
+
puts "[RustBackend] Compiling Rust crate..."
|
|
471
|
+
crate_dir = Rails.root.join("ext/rust_backend")
|
|
472
|
+
target_dir = Rails.root.join("lib/rust_backend")
|
|
473
|
+
FileUtils.mkdir_p(target_dir)
|
|
474
|
+
|
|
475
|
+
system(rust_build_env, "cargo build --release", chdir: crate_dir.to_s, exception: true)
|
|
476
|
+
|
|
477
|
+
source_so = crate_dir.join("target/release/librust_backend.so")
|
|
478
|
+
dest_so = target_dir.join("rust_backend.so")
|
|
479
|
+
|
|
480
|
+
FileUtils.cp(source_so, dest_so)
|
|
481
|
+
puts "[RustBackend] Built and copied extension to \#{dest_so}"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
desc "Run native Rust tests in ext/rust_backend"
|
|
485
|
+
task :test do
|
|
486
|
+
crate_dir = Rails.root.join("ext/rust_backend")
|
|
487
|
+
system(rust_build_env, "cargo test", chdir: crate_dir.to_s, exception: true)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
desc "Clean compiled Rust artifacts"
|
|
491
|
+
task :clean do
|
|
492
|
+
crate_dir = Rails.root.join("ext/rust_backend")
|
|
493
|
+
system(rust_build_env, "cargo clean", chdir: crate_dir.to_s)
|
|
494
|
+
FileUtils.rm_f(Rails.root.join("lib/rust_backend/rust_backend.so"))
|
|
495
|
+
puts "[RustBackend] Cleaned Rust build artifacts."
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
task test: "rust:build"
|
|
500
|
+
RUBY
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railst
|
|
4
|
+
module Terminal
|
|
5
|
+
def self.color?
|
|
6
|
+
$stdout.tty? && ENV["NO_COLOR"].nil?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.badge(text, bg: :green, fg: :black)
|
|
10
|
+
return "[#{text}]" unless color?
|
|
11
|
+
|
|
12
|
+
bg_code = case bg
|
|
13
|
+
when :green then "42"
|
|
14
|
+
when :red then "41"
|
|
15
|
+
when :yellow then "43"
|
|
16
|
+
when :blue then "44"
|
|
17
|
+
when :cyan then "46"
|
|
18
|
+
when :magenta then "45"
|
|
19
|
+
else "40"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
fg_code = case fg
|
|
23
|
+
when :black then "30"
|
|
24
|
+
when :white then "97"
|
|
25
|
+
when :bright_white then "1;37"
|
|
26
|
+
else "37"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
"\e[#{fg_code};#{bg_code}m\e[1m #{text} \e[0m"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.bold(text)
|
|
33
|
+
color? ? "\e[1m#{text}\e[0m" : text.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.dim(text)
|
|
37
|
+
color? ? "\e[90m#{text}\e[0m" : text.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.cyan(text)
|
|
41
|
+
color? ? "\e[36m#{text}\e[0m" : text.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.bold_cyan(text)
|
|
45
|
+
color? ? "\e[1;36m#{text}\e[0m" : text.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.yellow(text)
|
|
49
|
+
color? ? "\e[33m#{text}\e[0m" : text.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.bold_yellow(text)
|
|
53
|
+
color? ? "\e[1;33m#{text}\e[0m" : text.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.red(text)
|
|
57
|
+
color? ? "\e[31m#{text}\e[0m" : text.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.bold_red(text)
|
|
61
|
+
color? ? "\e[1;31m#{text}\e[0m" : text.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.green(text)
|
|
65
|
+
color? ? "\e[32m#{text}\e[0m" : text.to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.bold_green(text)
|
|
69
|
+
color? ? "\e[1;32m#{text}\e[0m" : text.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.magenta(text)
|
|
73
|
+
color? ? "\e[35m#{text}\e[0m" : text.to_s
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.bold_magenta(text)
|
|
77
|
+
color? ? "\e[1;35m#{text}\e[0m" : text.to_s
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/railst.rb
ADDED