use super::entry::{MutationRecord, TxAction, TxEntry};
use super::log::TxLog;
use crate::storage::StateRef;
use ryo_analysis::SymbolPath;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread::{self, JoinHandle};
use std::time::Instant;
enum LoggerMessage {
Log(TxAction),
LogWithDuration(TxAction, u64),
Shutdown,
}
pub struct TxLogger {
sender: Sender<LoggerMessage>,
handle: Option<JoinHandle<TxLog>>,
session_start: Instant,
}
impl TxLogger {
pub fn start(project_path: impl Into<PathBuf>, file_count: usize) -> Self {
let project_path: PathBuf = project_path.into();
let project_path_for_thread = project_path.clone();
let project_path_for_log = project_path.clone();
let (sender, receiver) = mpsc::channel();
let session_start = Instant::now();
let handle = thread::spawn(move || {
Self::background_worker(receiver, project_path_for_thread, file_count, session_start)
});
let logger = Self {
sender,
handle: Some(handle),
session_start,
};
logger.log(TxAction::SessionStart {
project_path: project_path_for_log,
file_count,
});
logger
}
fn background_worker(
receiver: Receiver<LoggerMessage>,
project_path: PathBuf,
_file_count: usize,
session_start: Instant,
) -> TxLog {
let mut log = TxLog::with_project(project_path.to_string_lossy().to_string());
let mut next_id: u64 = 0;
loop {
match receiver.recv() {
Ok(LoggerMessage::Log(action)) => {
let timestamp_ms = session_start.elapsed().as_millis() as u64;
log.push(TxEntry::new(next_id, timestamp_ms, action));
next_id += 1;
}
Ok(LoggerMessage::LogWithDuration(action, duration_us)) => {
let timestamp_ms = session_start.elapsed().as_millis() as u64;
log.push(
TxEntry::new(next_id, timestamp_ms, action).with_duration(duration_us),
);
next_id += 1;
}
Ok(LoggerMessage::Shutdown) => {
break;
}
Err(_) => {
break;
}
}
}
log.end_session();
log
}
pub fn log(&self, action: TxAction) {
let _ = self.sender.send(LoggerMessage::Log(action));
}
pub fn log_with_duration(&self, action: TxAction, duration_us: u64) {
let _ = self
.sender
.send(LoggerMessage::LogWithDuration(action, duration_us));
}
pub fn log_timed<F, R>(&self, action_fn: F, make_action: impl FnOnce(R) -> TxAction) -> R
where
F: FnOnce() -> R,
{
let start = Instant::now();
let result = action_fn();
let duration_us = start.elapsed().as_micros() as u64;
let action = make_action(result);
self.log_with_duration(action, duration_us);
unreachable!() }
pub fn log_goal(&self, query: &str, intent_type: &str, confidence: f64) {
self.log(TxAction::GoalSet {
query: query.to_string(),
intent_type: intent_type.to_string(),
confidence,
});
}
pub fn log_mutation(&self, mutation_type: &str, target: &str, changes: usize) {
self.log(TxAction::MutationApplied {
mutation_type: mutation_type.to_string(),
target: target.to_string(),
changes,
mutation_data: None,
file_path: None,
pre_state: None,
post_state: None,
affected_symbols: vec![],
});
}
pub fn record_mutation<M>(&self, mutation: &M, changes: usize)
where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
let serializable = mutation.to_serializable();
self.log(TxAction::MutationApplied {
mutation_type: mutation.mutation_type().to_string(),
target: mutation.describe(),
changes,
mutation_data: Some(serializable.to_json()),
file_path: None,
pre_state: None,
post_state: None,
affected_symbols: vec![],
});
}
pub fn record_mutation_for_file<M>(
&self,
mutation: &M,
changes: usize,
file_path: impl AsRef<Path>,
) where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
let serializable = mutation.to_serializable();
self.log(TxAction::MutationApplied {
mutation_type: mutation.mutation_type().to_string(),
target: mutation.describe(),
changes,
mutation_data: Some(serializable.to_json()),
file_path: Some(file_path.as_ref().to_path_buf()),
pre_state: None,
post_state: None,
affected_symbols: vec![],
});
}
pub fn record_mutation_tracked<M>(
&self,
mutation: &M,
changes: usize,
file_path: impl AsRef<Path>,
pre_state: StateRef,
post_state: StateRef,
) where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
let serializable = mutation.to_serializable();
self.log(TxAction::MutationApplied {
mutation_type: mutation.mutation_type().to_string(),
target: mutation.describe(),
changes,
mutation_data: Some(serializable.to_json()),
file_path: Some(file_path.as_ref().to_path_buf()),
pre_state: Some(pre_state),
post_state: Some(post_state),
affected_symbols: vec![],
});
}
pub fn record_mutation_with_symbols<M>(
&self,
mutation: &M,
changes: usize,
affected_symbols: Vec<SymbolPath>,
) where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
let serializable = mutation.to_serializable();
self.log(TxAction::MutationApplied {
mutation_type: mutation.mutation_type().to_string(),
target: mutation.describe(),
changes,
mutation_data: Some(serializable.to_json()),
file_path: None,
pre_state: None,
post_state: None,
affected_symbols,
});
}
pub fn record_mutation_full<M>(
&self,
mutation: &M,
changes: usize,
file_path: impl AsRef<Path>,
pre_state: StateRef,
post_state: StateRef,
affected_symbols: Vec<SymbolPath>,
) where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
let serializable = mutation.to_serializable();
self.log(TxAction::MutationApplied {
mutation_type: mutation.mutation_type().to_string(),
target: mutation.describe(),
changes,
mutation_data: Some(serializable.to_json()),
file_path: Some(file_path.as_ref().to_path_buf()),
pre_state: Some(pre_state),
post_state: Some(post_state),
affected_symbols,
});
}
pub fn log_mutation_with_data(
&self,
mutation_type: &str,
target: &str,
changes: usize,
data: serde_json::Value,
) {
self.log(TxAction::MutationApplied {
mutation_type: mutation_type.to_string(),
target: target.to_string(),
changes,
mutation_data: Some(data),
file_path: None,
pre_state: None,
post_state: None,
affected_symbols: vec![],
});
}
pub fn log_mutation_batch(&self, mutations: Vec<MutationRecord>, total_changes: usize) {
self.log(TxAction::MutationBatch {
mutations,
total_changes,
});
}
pub fn log_file_loaded(&self, path: &Path, size_bytes: usize) {
self.log(TxAction::FileLoaded {
path: path.to_path_buf(),
size_bytes,
});
}
pub fn log_file_modified(&self, path: &Path, changes: usize) {
self.log(TxAction::FileModified {
path: path.to_path_buf(),
changes,
});
}
pub fn log_file_written(&self, path: &Path) {
self.log(TxAction::FileWritten {
path: path.to_path_buf(),
});
}
pub fn log_compile_check(&self, success: bool, errors: Vec<String>) {
self.log(TxAction::CompileCheck {
success,
error_count: errors.len(),
errors,
});
}
pub fn checkpoint(&self, name: &str) {
self.log(TxAction::Checkpoint {
name: name.to_string(),
});
}
pub fn log_undo(&self, target_id: u64) {
self.log(TxAction::Undo { target_id });
}
pub fn log_redo(&self, target_id: u64) {
self.log(TxAction::Redo { target_id });
}
pub fn log_custom(&self, name: &str, data: serde_json::Value) {
self.log(TxAction::Custom {
name: name.to_string(),
data,
});
}
pub fn elapsed_ms(&self) -> u64 {
self.session_start.elapsed().as_millis() as u64
}
pub fn finish(mut self) -> TxLog {
let _ = self.sender.send(LoggerMessage::Shutdown);
match self.handle.take() {
Some(handle) => handle.join().unwrap_or_else(|_| TxLog::new()),
None => TxLog::new(),
}
}
pub fn finish_and_dump(self, path: &Path) -> std::io::Result<TxLog> {
let log = self.finish();
log.dump_json(path)?;
Ok(log)
}
}
impl Drop for TxLogger {
fn drop(&mut self) {
let _ = self.sender.send(LoggerMessage::Shutdown);
}
}
#[cfg(test)]
struct TxLoggerSync {
log: TxLog,
}
#[cfg(test)]
impl TxLoggerSync {
fn new(project_path: impl Into<std::path::PathBuf>, file_count: usize) -> Self {
let project_path = project_path.into();
let mut log = TxLog::with_project(project_path.to_string_lossy().to_string());
log.log(TxAction::SessionStart {
project_path,
file_count,
});
Self { log }
}
fn log_goal(&mut self, query: &str, intent_type: &str, confidence: f64) {
self.log.log(TxAction::GoalSet {
query: query.to_string(),
intent_type: intent_type.to_string(),
confidence,
});
}
fn log_mutation(&mut self, mutation_type: &str, target: &str, changes: usize) {
self.log.log(TxAction::MutationApplied {
mutation_type: mutation_type.to_string(),
target: target.to_string(),
changes,
mutation_data: None,
file_path: None,
pre_state: None,
post_state: None,
affected_symbols: vec![],
});
}
fn checkpoint(&mut self, name: &str) {
self.log.log(TxAction::Checkpoint {
name: name.to_string(),
});
}
fn finish(mut self) -> TxLog {
self.log.end_session();
self.log
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_async_logger() {
let logger = TxLogger::start("/test/project", 10);
logger.log_goal("rename foo", "RenameIdent", 0.9);
logger.log_mutation("Rename", "foo -> bar", 5);
logger.checkpoint("after_rename");
logger.log_file_modified(Path::new("/test.rs"), 3);
thread::sleep(Duration::from_millis(10));
let log = logger.finish();
assert!(log.len() >= 4);
let summary = log.summary();
assert!(summary.total_mutations >= 1);
assert!(summary.checkpoints.contains(&"after_rename".to_string()));
}
#[test]
fn test_sync_logger() {
let mut logger = TxLoggerSync::new("/test/project", 10);
logger.log_goal("test query", "TestIntent", 1.0);
logger.log_mutation("AddField", "MyStruct.field", 1);
logger.checkpoint("done");
let log = logger.finish();
assert_eq!(log.len(), 4);
let summary = log.summary();
assert_eq!(summary.total_mutations, 1);
}
}