Skip to content

Commit 0eaa3d8

Browse files
committed
perf: TTS 음성 데이터 읽기 최적화로 LOH 할당 제거
- MemoryStream 제거하고 IMemoryOwner 직접 증분 복사 방식으로 변경 - 메모리 할당 최적화: 동적 버퍼 크기 조정 및 메모리 재사용 - ArrayPool 활용으로 대규모 힙(LOH) 할당 방지 - 스트림 처리 성능 및 메모리 관리 개선
1 parent 42eb294 commit 0eaa3d8

File tree

1 file changed

+29
-18
lines changed

1 file changed

+29
-18
lines changed

ProjectVG.Infrastructure/Integrations/TextToSpeechClient/TextToSpeechClient.cs

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,39 +88,50 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
8888
{
8989
const int chunkSize = 32768; // 32KB 청크 크기
9090
byte[]? readBuffer = null;
91-
MemoryStream? memoryStream = null;
91+
IMemoryOwner<byte>? owner = null;
9292

9393
try
9494
{
9595
readBuffer = _arrayPool.Rent(chunkSize);
96-
memoryStream = new MemoryStream();
97-
9896
using var stream = await content.ReadAsStreamAsync();
99-
int bytesRead;
10097

101-
// 청크 단위로 데이터 읽어서 MemoryStream에 복사
102-
while ((bytesRead = await stream.ReadAsync(readBuffer, 0, chunkSize)) > 0)
98+
// 초기 버퍼 렌트(증분 확장 전략)
99+
owner = MemoryPool<byte>.Shared.Rent(chunkSize);
100+
int total = 0;
101+
while (true)
103102
{
104-
await memoryStream.WriteAsync(readBuffer, 0, bytesRead);
105-
}
106-
107-
var totalSize = (int)memoryStream.Length;
103+
// 여유 공간 없으면 확장
104+
if (total == owner.Memory.Length)
105+
{
106+
var newOwner = MemoryPool<byte>.Shared.Rent(Math.Min(owner.Memory.Length * 2, int.MaxValue));
107+
owner.Memory.Span.Slice(0, total).CopyTo(newOwner.Memory.Span);
108+
owner.Dispose();
109+
owner = newOwner;
110+
}
108111

109-
// ArrayPool에서 최종 데이터 크기만큼 메모리 할당
110-
var resultMemoryOwner = MemoryPool<byte>.Shared.Rent(totalSize);
112+
int toRead = Math.Min(chunkSize, owner.Memory.Length - total);
113+
int bytesRead = await stream.ReadAsync(readBuffer, 0, toRead);
114+
if (bytesRead == 0) break;
115+
readBuffer.AsSpan(0, bytesRead).CopyTo(owner.Memory.Span.Slice(total));
116+
total += bytesRead;
117+
}
111118

112-
// MemoryStream에서 최종 메모리로 복사
113-
memoryStream.Position = 0;
114-
await memoryStream.ReadAsync(resultMemoryOwner.Memory.Slice(0, totalSize));
119+
if (total == 0)
120+
{
121+
owner.Dispose();
122+
_logger.LogDebug("[TTS][ArrayPool] 비어있는 오디오 스트림");
123+
return (null, 0);
124+
}
115125

116126
_logger.LogDebug("[TTS][ArrayPool] 음성 데이터 읽기 완료: {Size} bytes, 청크 크기: {ChunkSize}",
117-
totalSize, chunkSize);
127+
total, chunkSize);
118128

119-
return (resultMemoryOwner, totalSize);
129+
return (owner, total);
120130
}
121131
catch (Exception ex)
122132
{
123133
_logger.LogError(ex, "[TTS][ArrayPool] 음성 데이터 읽기 실패");
134+
owner?.Dispose();
124135
return (null, 0);
125136
}
126137
finally
@@ -129,7 +140,7 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
129140
{
130141
_arrayPool.Return(readBuffer);
131142
}
132-
memoryStream?.Dispose();
143+
// owner는 정상 경로에서 호출자에게 반환됨. 예외 시 위에서 Dispose 처리.
133144
}
134145
}
135146

0 commit comments

Comments
 (0)