Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions src/infrastructure/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,13 @@ impl VoicevoxCore {
///
/// Returns an error if the model file cannot be opened or the core fails to unload it.
pub fn unload_voice_model_by_path(&self, model_path: &Path) -> Result<()> {
let model = open_voice_model_file(model_path)?;
let voice_model_id = model.id();
let voice_model_id = open_voice_model_file(model_path)?.id();

self.synthesizer
.unload_voice_model(voice_model_id)
.map_err(|e| anyhow!("Failed to unload model: {e}"))
.map_err(|e| anyhow!("Failed to unload model: {e}"))?;

crate::infrastructure::memory::release_unused_allocator_memory();
Ok(())
}
}
19 changes: 13 additions & 6 deletions src/infrastructure/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ use tokio::net::{UnixListener, UnixStream};
use tokio::signal;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::time::timeout;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};

use crate::infrastructure::daemon::state::DaemonState;
use crate::infrastructure::ipc::{DaemonRequest, MAX_DAEMON_REQUEST_FRAME_BYTES, OwnedResponse};
use crate::infrastructure::ipc::{
DaemonRequest, MAX_DAEMON_REQUEST_FRAME_BYTES, MAX_DAEMON_RESPONSE_FRAME_BYTES, OwnedResponse,
};

const SOCKET_DIR_MODE: u32 = 0o700;
const SOCKET_FILE_MODE: u32 = 0o600;
Expand Down Expand Up @@ -115,12 +117,17 @@ async fn handle_client_with_limit(
state: Arc<DaemonState>,
permits: Arc<Semaphore>,
) -> Result<()> {
let codec = LengthDelimitedCodec::builder()
let request_codec = LengthDelimitedCodec::builder()
.max_frame_length(MAX_DAEMON_REQUEST_FRAME_BYTES)
.new_codec();
let mut framed = Framed::new(stream, codec);
let response_codec = LengthDelimitedCodec::builder()
.max_frame_length(MAX_DAEMON_RESPONSE_FRAME_BYTES)
.new_codec();
let (reader, writer) = stream.into_split();
let mut framed_read = FramedRead::new(reader, request_codec);
let mut framed_write = FramedWrite::new(writer, response_codec);

while let Some(frame) = timeout(CLIENT_IDLE_TIMEOUT, framed.next())
while let Some(frame) = timeout(CLIENT_IDLE_TIMEOUT, framed_read.next())
.await
.map_err(|_| anyhow!("Client idle timeout"))?
{
Expand Down Expand Up @@ -148,7 +155,7 @@ async fn handle_client_with_limit(
break;
};

if let Err(error) = framed.send(response_data.into()).await {
if let Err(error) = framed_write.send(response_data.into()).await {
log_client_error("Client stream write error", &error);
break;
}
Expand Down
9 changes: 6 additions & 3 deletions src/infrastructure/daemon/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ impl DaemonState {
/// Returns an error if VOICEVOX core initialization fails, model discovery fails,
/// or the style-to-model mapping cannot be constructed.
pub fn new() -> Result<Self> {
let core = crate::infrastructure::core::VoicevoxCore::new()?;
let catalog = ModelCatalog::new(&core)?;
let synthesis_executor = DaemonSynthesisExecutor::new(core);
let catalog_core = crate::infrastructure::core::VoicevoxCore::new()?;
let catalog = ModelCatalog::new(&catalog_core)?;
drop(catalog_core);
crate::infrastructure::memory::release_unused_allocator_memory();

let synthesis_executor = DaemonSynthesisExecutor::new();
let synthesis_policy = SerializedSynthesisPolicy::new(synthesis_executor);

Ok(Self {
Expand Down
48 changes: 32 additions & 16 deletions src/infrastructure/daemon/state/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ use crate::infrastructure::core::VoicevoxCore;
use super::catalog::{ModelCatalog, TargetResolution};
use super::result::{DaemonServiceError, DaemonServiceErrorKind, DaemonServiceResult};

pub(super) struct DaemonSynthesisExecutor {
core: VoicevoxCore,
}
pub(super) struct DaemonSynthesisExecutor;

/// RAII guard that unloads a voice model on drop.
///
Expand All @@ -20,6 +18,14 @@ struct ModelUnloadGuard<'a> {
model_path: Option<&'a Path>,
}

struct AllocatorReliefGuard;

impl Drop for AllocatorReliefGuard {
fn drop(&mut self) {
crate::infrastructure::memory::release_unused_allocator_memory();
}
}

impl Drop for ModelUnloadGuard<'_> {
fn drop(&mut self) {
let Some(model_path) = self.model_path else {
Expand All @@ -40,12 +46,12 @@ impl Drop for ModelUnloadGuard<'_> {
}

impl DaemonSynthesisExecutor {
pub(super) fn new(core: VoicevoxCore) -> Self {
Self { core }
pub(super) fn new() -> Self {
Self
}

pub(super) fn synthesize(
&self,
&mut self,
catalog: &ModelCatalog,
text: String,
requested_id: u32,
Expand All @@ -62,7 +68,15 @@ impl DaemonSynthesisExecutor {
};
let model_path = catalog.get_model_path(model_id);

if let Err(error) = self.core.load_specific_model(model_id) {
let _allocator_relief = AllocatorReliefGuard;
let core = VoicevoxCore::new().map_err(|error| {
DaemonServiceError::new(
DaemonServiceErrorKind::ModelLoadFailed,
format!("Failed to initialize VOICEVOX core for synthesis: {error}"),
)
})?;

if let Err(error) = core.load_specific_model(model_id) {
crate::infrastructure::logging::error(&format!(
"Failed to load model {model_id}: {error}"
));
Expand All @@ -72,16 +86,18 @@ impl DaemonSynthesisExecutor {
));
}

// RAII guard ensures the model is always unloaded, even on panic or
// task cancellation. Matches DaemonRequestHandling.tla ClientDisconnect:
// mutex_holder = c => model_loaded' = FALSE
let _model_guard = ModelUnloadGuard {
core: &self.core,
model_id,
model_path,
};
let synthesis_result = {
// RAII guard ensures the model is always unloaded, even on panic or
// task cancellation. Matches DaemonRequestHandling.tla ClientDisconnect:
// mutex_holder = c => model_loaded' = FALSE
let _model_guard = ModelUnloadGuard {
core: &core,
model_id,
model_path,
};

let synthesis_result = self.core.synthesize_with_rate(&text, style_id, rate);
core.synthesize_with_rate(&text, style_id, rate)
};

match synthesis_result {
Ok(wav_data) => Ok(DaemonServiceResult::SynthesizeResult { wav_data }),
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/daemon/state/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl SerializedSynthesisPolicy {
requested_id: u32,
rate: f32,
) -> Result<DaemonServiceResult, DaemonServiceError> {
let executor = self.executor.lock().await;
let mut executor = self.executor.lock().await;
executor.synthesize(catalog, text, requested_id, rate)
}
}
23 changes: 23 additions & 0 deletions src/infrastructure/memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// Asks the platform allocator to return unused process memory to the OS.
///
/// This is a best-effort RSS reduction hook for large native allocations released
/// by VOICEVOX Core. The return value is the allocator-reported released byte
/// count when the platform exposes one.
#[cfg(target_os = "macos")]
pub fn release_unused_allocator_memory() -> usize {
use libc::size_t;
use std::ffi::c_void;

unsafe extern "C" {
fn malloc_zone_pressure_relief(zone: *mut c_void, goal: size_t) -> size_t;
}

// A null zone asks libmalloc to examine all zones. A zero goal requests as
// much relief as the allocator can currently provide.
unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) as usize }
}

#[cfg(not(target_os = "macos"))]
pub fn release_unused_allocator_memory() -> usize {
0
}
1 change: 1 addition & 0 deletions src/infrastructure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod download;
pub mod ipc;
pub mod logging;
pub mod mcp_instructions;
pub mod memory;
pub mod onnxruntime;
pub mod openjtalk;
pub mod paths;
Expand Down
Loading