Skip to content

Commit ca62d4d

Browse files
committed
perf: TTS 파이프라인 ArrayPool 완전 적용으로 LOH 제거
TextToSpeechResponse에 IMemoryOwner 필드 추가 TextToSpeechClient에서 MemoryPool.Shared.Rent 사용 ChatTTSProcessor에서 WithAudioMemory로 변경 이중/삼중 LOH 할당 제거로 GC 압박 대폭 감소
1 parent 81dae08 commit ca62d4d

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ public async Task ProcessAsync(ChatProcessContext context)
4646
var processedCount = 0;
4747

4848
foreach (var (idx, ttsResult) in ttsResults.OrderBy(x => x.idx)) {
49-
if (ttsResult.Success == true && ttsResult.AudioData != null) {
49+
if (ttsResult.Success == true && ttsResult.AudioMemoryOwner != null) {
5050
var segment = context.Segments?[idx];
5151
if (segment != null && context.Segments != null) {
52-
context.Segments[idx] = segment.WithAudioData(ttsResult.AudioData, ttsResult.ContentType!, ttsResult.AudioLength ?? 0f);
52+
context.Segments[idx] = segment.WithAudioMemory(
53+
ttsResult.AudioMemoryOwner,
54+
ttsResult.AudioDataSize,
55+
ttsResult.ContentType!,
56+
ttsResult.AudioLength ?? 0f);
5357
}
5458

5559
if (ttsResult.AudioLength.HasValue) {

ProjectVG.Infrastructure/Integrations/TextToSpeechClient/Models/TextToSpeechResponse.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json.Serialization;
2+
using System.Buffers;
23

34
namespace ProjectVG.Infrastructure.Integrations.TextToSpeechClient.Models
45
{
@@ -17,11 +18,23 @@ public class TextToSpeechResponse
1718
public string? ErrorMessage { get; set; }
1819

1920
/// <summary>
20-
/// 오디오 데이터 (바이트 배열)
21+
/// 오디오 데이터 (바이트 배열) - 레거시 호환성용
2122
/// </summary>
2223
[JsonIgnore]
2324
public byte[]? AudioData { get; set; }
2425

26+
/// <summary>
27+
/// ArrayPool 기반 오디오 메모리 소유자 (LOH 방지)
28+
/// </summary>
29+
[JsonIgnore]
30+
public IMemoryOwner<byte>? AudioMemoryOwner { get; set; }
31+
32+
/// <summary>
33+
/// 실제 오디오 데이터 크기
34+
/// </summary>
35+
[JsonIgnore]
36+
public int AudioDataSize { get; set; }
37+
2538
/// <summary>
2639
/// 오디오 길이 (초)
2740
/// </summary>

ProjectVG.Infrastructure/Integrations/TextToSpeechClient/TextToSpeechClient.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
5050
return voiceResponse;
5151
}
5252

53-
// 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
54-
voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
53+
// ArrayPool 기반으로 음성 데이터 읽기 (LOH 방지)
54+
var (memoryOwner, dataSize) = await ReadAudioDataWithPoolAsync(response.Content);
55+
voiceResponse.AudioMemoryOwner = memoryOwner;
56+
voiceResponse.AudioDataSize = dataSize;
5557
voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString();
5658

5759
if (response.Headers.Contains("X-Audio-Length"))
@@ -64,7 +66,7 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
6466
}
6567

6668
_logger.LogDebug("[TTS][Response] 오디오 길이: {AudioLength:F2}초, ContentType: {ContentType}, 바이트: {Length}, 소요시간: {Elapsed}ms",
67-
voiceResponse.AudioLength, voiceResponse.ContentType, voiceResponse.AudioData?.Length ?? 0, elapsed);
69+
voiceResponse.AudioLength, voiceResponse.ContentType, voiceResponse.AudioDataSize, elapsed);
6870

6971
return voiceResponse;
7072
}
@@ -82,42 +84,50 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
8284
/// <summary>
8385
/// ArrayPool을 사용하여 스트림 기반으로 음성 데이터를 읽습니다 (LOH 할당 방지)
8486
/// </summary>
85-
private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content)
87+
private async Task<(IMemoryOwner<byte>?, int)> ReadAudioDataWithPoolAsync(HttpContent content)
8688
{
8789
const int chunkSize = 32768; // 32KB 청크 크기
88-
byte[]? buffer = null;
90+
byte[]? readBuffer = null;
8991
MemoryStream? memoryStream = null;
9092

9193
try
9294
{
93-
buffer = _arrayPool.Rent(chunkSize);
95+
readBuffer = _arrayPool.Rent(chunkSize);
9496
memoryStream = new MemoryStream();
9597

9698
using var stream = await content.ReadAsStreamAsync();
9799
int bytesRead;
98100

99101
// 청크 단위로 데이터 읽어서 MemoryStream에 복사
100-
while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0)
102+
while ((bytesRead = await stream.ReadAsync(readBuffer, 0, chunkSize)) > 0)
101103
{
102-
await memoryStream.WriteAsync(buffer, 0, bytesRead);
104+
await memoryStream.WriteAsync(readBuffer, 0, bytesRead);
103105
}
104106

105-
var result = memoryStream.ToArray();
107+
var totalSize = (int)memoryStream.Length;
108+
109+
// ArrayPool에서 최종 데이터 크기만큼 메모리 할당
110+
var resultMemoryOwner = MemoryPool<byte>.Shared.Rent(totalSize);
111+
112+
// MemoryStream에서 최종 메모리로 복사
113+
memoryStream.Position = 0;
114+
await memoryStream.ReadAsync(resultMemoryOwner.Memory.Slice(0, totalSize));
115+
106116
_logger.LogDebug("[TTS][ArrayPool] 음성 데이터 읽기 완료: {Size} bytes, 청크 크기: {ChunkSize}",
107-
result.Length, chunkSize);
117+
totalSize, chunkSize);
108118

109-
return result;
119+
return (resultMemoryOwner, totalSize);
110120
}
111121
catch (Exception ex)
112122
{
113123
_logger.LogError(ex, "[TTS][ArrayPool] 음성 데이터 읽기 실패");
114-
return null;
124+
return (null, 0);
115125
}
116126
finally
117127
{
118-
if (buffer != null)
128+
if (readBuffer != null)
119129
{
120-
_arrayPool.Return(buffer);
130+
_arrayPool.Return(readBuffer);
121131
}
122132
memoryStream?.Dispose();
123133
}

0 commit comments

Comments
 (0)