Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
{os: "ubuntu-latest", arch: "x64"},
]
node-version: [ 18, 20, 22, 24 ]
binding: [ cpp, rust ]
steps:
- name: Set up Python
uses: actions/setup-python@v4
Expand All @@ -35,13 +36,31 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
if: ${{ matrix.binding == 'rust' }}

- name: Install Dependencies
run: npm install

- name: Continuous Integration
run: npm run ci
- name: Build C++ Binding
if: ${{ matrix.binding == 'cpp' }}
run: npm run build

- name: Build Rust Binding
if: ${{ matrix.binding == 'rust' }}
run: npm run build:rs

- name: Test C++ Binding
if: ${{ matrix.binding == 'cpp' }}
run: npm test

- name: Test Rust Binding
if: ${{ matrix.binding == 'rust' }}
run: npm run test:rs

- name: Code Coverage
if: ${{ matrix.binding == 'cpp' }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ logdir*

tmphome*

# Rust build artifacts
crates/*/target/
Cargo.lock
*.node
62 changes: 62 additions & 0 deletions crates/xprofiler-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[package]
name = "xprofiler-rs"
version = "3.1.0"
edition = "2021"
license = "BSD-2-Clause"
description = "Node.js addon for runtime profiling and performance monitoring"
repository = "https://github.com/X-Profiler/xprofiler"
authors = ["X-Profiler Contributors"]

[lib]
crate-type = ["cdylib"]

[dependencies]
# napi-rs for Node.js bindings (latest v3)
napi = { version = "3", default-features = false, features = ["napi8", "serde-json"] }
napi-derive = "3"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Concurrency primitives
parking_lot = "0.12"
once_cell = "1"

# Date/time handling
chrono = "0.4"

# Error handling
thiserror = "2"

# Logging/tracing
tracing = "0.1"

# Async runtime for IPC
tokio = { version = "1", features = ["rt", "sync", "time", "io-util", "net", "macros"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2"
nix = { version = "0.30", features = ["socket", "uio", "process", "signal", "fs"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_Pipes",
"Win32_System_Threading",
"Win32_System_Performance",
"Win32_Security",
"Win32_Storage_FileSystem",
]}

[build-dependencies]
napi-build = "2"

[profile.release]
lto = true
opt-level = 3
strip = "symbols"

[profile.dev]
opt-level = 0
debug = true
5 changes: 5 additions & 0 deletions crates/xprofiler-rs/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extern crate napi_build;

fn main() {
napi_build::setup();
}
107 changes: 107 additions & 0 deletions crates/xprofiler-rs/src/commands/listener.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Commands listener thread management

use super::parser::handle_command;
use crate::config;
use crate::ipc::{self, IpcServer, MessageHandler};
use crate::utils;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::sync::Arc;

/// Global commands listener
static COMMANDS_LISTENER: Lazy<Mutex<Option<CommandsListener>>> = Lazy::new(|| Mutex::new(None));

/// Commands listener state
struct CommandsListener {
server: Box<dyn IpcServer>,
}

/// Start the commands listener thread
pub fn start_commands_listener() -> Result<(), String> {
let mut listener = COMMANDS_LISTENER.lock();

// If already started, just return success
if listener.is_some() {
return Ok(());
}

let cfg = config::get_config();
let pid = utils::get_pid();

// Create the IPC server
let mut server = ipc::create_server(&cfg.log_dir, pid);

// Create the message handler
let handler: MessageHandler = Arc::new(|message| {
handle_command(&message)
});

// Start the server
// If the path is too long (common in test fixtures), just skip starting the server
// The JS layer already logs the warning via checkSocketPath()
if let Err(e) = server.start(handler) {
let error_msg = e.to_string();
if error_msg.contains("SUN_LEN") || error_msg.contains("too long") {
// Path too long - this is handled gracefully, don't fail
eprintln!("[xprofiler] IPC server not started: socket path too long");
return Ok(());
}
return Err(format!("Failed to start IPC server: {}", e));
}

*listener = Some(CommandsListener { server });

Ok(())
}

/// Stop the commands listener thread
pub fn stop_commands_listener() -> Result<(), String> {
let mut listener = COMMANDS_LISTENER.lock();

if let Some(mut l) = listener.take() {
l.server.stop().map_err(|e| format!("Failed to stop IPC server: {}", e))?;
}

Ok(())
}

/// Check if the commands listener is running
pub fn is_commands_listener_running() -> bool {
COMMANDS_LISTENER
.lock()
.as_ref()
.map_or(false, |l| l.server.is_running())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_start_stop_listener() {
// Set up a test config with a temp directory
let temp_dir = std::env::temp_dir().join("xprofiler-test-listener");
std::fs::create_dir_all(&temp_dir).ok();

config::update_config(|c| {
c.log_dir = temp_dir.to_string_lossy().to_string();
}).unwrap();

// Start the listener
let result = start_commands_listener();
assert!(result.is_ok(), "Failed to start: {:?}", result);
assert!(is_commands_listener_running());

// Try to start again (should succeed silently)
let result = start_commands_listener();
assert!(result.is_ok());

// Stop the listener
let result = stop_commands_listener();
assert!(result.is_ok());
assert!(!is_commands_listener_running());

// Cleanup
std::fs::remove_dir_all(&temp_dir).ok();
}
}
61 changes: 61 additions & 0 deletions crates/xprofiler-rs/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Command system for xprofiler-rs
//!
//! This module handles commands from xprofctl CLI via IPC.

pub mod listener;
pub mod parser;

use serde::{Deserialize, Serialize};

/// Command request from xprofctl
#[derive(Debug, Deserialize)]
pub struct CommandRequest {
pub traceid: String,
pub cmd: String,
#[serde(default)]
pub thread_id: Option<i64>,
#[serde(default)]
pub options: Option<serde_json::Value>,
}

/// Helper to get profiling_time from options
impl CommandRequest {
pub fn profiling_time(&self) -> Option<u64> {
self.options.as_ref()?.get("profiling_time")?.as_u64()
}

pub fn filepath(&self) -> Option<String> {
self.options.as_ref()?.get("filepath")?.as_str().map(|s| s.to_string())
}
}

/// Command response
#[derive(Debug, Serialize)]
pub struct CommandResponse {
pub ok: bool,
pub traceid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}

impl CommandResponse {
pub fn success(traceid: &str, data: Option<serde_json::Value>) -> Self {
Self {
ok: true,
traceid: traceid.to_string(),
data,
message: None,
}
}

pub fn error(traceid: &str, message: &str) -> Self {
Self {
ok: false,
traceid: traceid.to_string(),
data: None,
message: Some(message.to_string()),
}
}
}
Loading
Loading