-
Notifications
You must be signed in to change notification settings - Fork 0
perf: ArrayPool 기반 LOH 최적화로 음성 데이터 메모리 효율성 개선 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,29 +1,50 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Buffers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace ProjectVG.Application.Models.Chat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public record ChatSegment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string Content { get; init; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public int Order { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string? Emotion { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public List<string>? Actions { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public byte[]? AudioData { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string? AudioContentType { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public float? AudioLength { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 스트림 기반 음성 데이터 처리를 위한 새로운 프로퍼티 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public IMemoryOwner<byte>? AudioMemoryOwner { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public int AudioDataSize { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasContent => !string.IsNullOrEmpty(Content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasAudio => AudioData != null && AudioData.Length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasAudio => (AudioData != null && AudioData.Length > 0) || (AudioMemoryOwner != null && AudioDataSize > 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool IsEmpty => !HasContent && !HasActions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasEmotion => !string.IsNullOrEmpty(Emotion); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasActions => Actions != null && Actions.Any(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 메모리 효율적인 방식으로 음성 데이터에 접근합니다 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ReadOnlySpan<byte> GetAudioSpan() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (AudioMemoryOwner != null && AudioDataSize > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (AudioData != null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new ReadOnlySpan<byte>(AudioData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ReadOnlySpan<byte>.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -51,12 +72,55 @@ public static ChatSegment CreateAction(string action, int order = 0) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Method to add audio data (returns new record instance) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ChatSegment WithAudioData(byte[] audioData, string audioContentType, float audioLength) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this with | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioData = audioData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioContentType = audioContentType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioLength = audioLength | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this with | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioData = audioData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioContentType = audioContentType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioLength = audioLength | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 메모리 효율적인 방식으로 음성 데이터를 추가합니다 (LOH 방지) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this with | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioMemoryOwner = audioMemoryOwner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioDataSize = audioDataSize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioContentType = audioContentType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioLength = audioLength, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 기존 AudioData는 null로 설정하여 중복 저장 방지 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioData = null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+86
to
97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 소유권 이전 시 크기/수명 검증 및 IDisposable 구현 권장
아래 보강을 권장합니다. - public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
+ public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
{
+ if (audioMemoryOwner is null) throw new ArgumentNullException(nameof(audioMemoryOwner));
+ if (audioDataSize < 0 || audioDataSize > audioMemoryOwner.Memory.Length)
+ throw new ArgumentOutOfRangeException(nameof(audioDataSize));
return this with
{
AudioMemoryOwner = audioMemoryOwner,
AudioDataSize = audioDataSize,
AudioContentType = audioContentType,
AudioLength = audioLength,
// 기존 AudioData는 null로 설정하여 중복 저장 방지
AudioData = null
};
}또한, 레코드 선언부에 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 음성 데이터를 배열로 변환합니다 (필요한 경우에만 사용) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public byte[]? GetAudioDataAsArray() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (AudioData != null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return AudioData; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (AudioMemoryOwner != null && AudioDataSize > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var span = AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return span.ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 리소스 해제 (IMemoryOwner 해제) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Dispose() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AudioMemoryOwner?.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+118
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion IDisposable 비구현 상태의 Dispose 메서드 메서드만 있고 인터페이스를 구현하지 않아 호출 누락 위험이 큽니다. // 선언부 예시 (파일 상단 선언부 수정 필요)
public record ChatSegment : IDisposable
{
// ...
public void Dispose() => AudioMemoryOwner?.Dispose();
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,4 @@ | ||||||||||||||||||||||||||
| using System.Buffers; | ||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||
| using System.Text.Json; | ||||||||||||||||||||||||||
| using Microsoft.Extensions.Logging; | ||||||||||||||||||||||||||
|
|
@@ -9,6 +10,8 @@ public class TextToSpeechClient : ITextToSpeechClient | |||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| private readonly HttpClient _httpClient; | ||||||||||||||||||||||||||
| private readonly ILogger<TextToSpeechClient> _logger; | ||||||||||||||||||||||||||
| private static readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared; | ||||||||||||||||||||||||||
| private const int MaxPoolSize = 1024 * 1024; // 1MB max pooled size | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| public TextToSpeechClient(HttpClient httpClient, ILogger<TextToSpeechClient> logger) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
|
|
@@ -47,7 +50,8 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re | |||||||||||||||||||||||||
| return voiceResponse; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| voiceResponse.AudioData = await response.Content.ReadAsByteArrayAsync(); | ||||||||||||||||||||||||||
| // 스트림 기반으로 음성 데이터 읽기 (LOH 방지) | ||||||||||||||||||||||||||
| voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content); | ||||||||||||||||||||||||||
| voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString(); | ||||||||||||||||||||||||||
|
Comment on lines
+53
to
55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 오디오 읽기 실패/빈 결과 처리 누락 및 LOH 목표 불충족
아래처럼 실패 시 에러로 처리하고, 후속 PR에서 IMemoryOwner 기반 반환(또는 스트리밍 전달)로 전환을 검토하세요. - // 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
- voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
+ // 스트림 기반으로 음성 데이터 읽기
+ voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
+ if (voiceResponse.AudioData == null || voiceResponse.AudioData.Length == 0)
+ {
+ voiceResponse.Success = false;
+ voiceResponse.ErrorMessage = "오디오 데이터를 읽지 못했습니다.";
+ return voiceResponse;
+ }추가 제안(별도 변경): 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (response.Headers.Contains("X-Audio-Length")) | ||||||||||||||||||||||||||
|
|
@@ -75,6 +79,50 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||
| /// ArrayPool을 사용하여 스트림 기반으로 음성 데이터를 읽습니다 (LOH 할당 방지) | ||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||
| private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| const int chunkSize = 32768; // 32KB 청크 크기 | ||||||||||||||||||||||||||
| byte[]? buffer = null; | ||||||||||||||||||||||||||
| MemoryStream? memoryStream = null; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| buffer = _arrayPool.Rent(chunkSize); | ||||||||||||||||||||||||||
| memoryStream = new MemoryStream(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| using var stream = await content.ReadAsStreamAsync(); | ||||||||||||||||||||||||||
| int bytesRead; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 청크 단위로 데이터 읽어서 MemoryStream에 복사 | ||||||||||||||||||||||||||
| while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| await memoryStream.WriteAsync(buffer, 0, bytesRead); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var result = memoryStream.ToArray(); | ||||||||||||||||||||||||||
| _logger.LogDebug("[TTS][ArrayPool] 음성 데이터 읽기 완료: {Size} bytes, 청크 크기: {ChunkSize}", | ||||||||||||||||||||||||||
| result.Length, chunkSize); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| _logger.LogError(ex, "[TTS][ArrayPool] 음성 데이터 읽기 실패"); | ||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| finally | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| if (buffer != null) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| _arrayPool.Return(buffer); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| memoryStream?.Dispose(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
+82
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion HTTP 리소스/스트림 수명 및 버퍼 관리 개선
아래와 같이 보완을 제안합니다. - private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content)
+ private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content, CancellationToken ct = default)
{
const int chunkSize = 32768; // 32KB 청크 크기
byte[]? buffer = null;
MemoryStream? memoryStream = null;
try
{
buffer = _arrayPool.Rent(chunkSize);
- memoryStream = new MemoryStream();
+ var capacity = content.Headers.ContentLength is long len && len <= int.MaxValue
+ ? (int)len
+ : 0;
+ memoryStream = capacity > 0 ? new MemoryStream(capacity) : new MemoryStream();
- using var stream = await content.ReadAsStreamAsync();
+ using var stream = await content.ReadAsStreamAsync(ct);
int bytesRead;
// 청크 단위로 데이터 읽어서 MemoryStream에 복사
- while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0)
+ while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize, ct)) > 0)
{
- await memoryStream.WriteAsync(buffer, 0, bytesRead);
+ await memoryStream.WriteAsync(buffer, 0, bytesRead, ct);
}
@@
finally
{
if (buffer != null)
{
- _arrayPool.Return(buffer);
+ _arrayPool.Return(buffer, clearArray: false);
}
memoryStream?.Dispose();
}
}또한, 상위 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| private string GetErrorMessageForStatusCode(int statusCode, string reasonPhrase) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| return $"HTTP {statusCode}: {reasonPhrase}"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Span 접근 자체는 적절하나 크기 검증 필요
AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize)는AudioDataSize가 실제 메모리 길이보다 클 경우 예외를 유발합니다. 방어적 체크를 추가하세요.public ReadOnlySpan<byte> GetAudioSpan() { - if (AudioMemoryOwner != null && AudioDataSize > 0) + if (AudioMemoryOwner != null && AudioDataSize > 0) { - return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize); + var mem = AudioMemoryOwner.Memory; + var size = Math.Min(AudioDataSize, mem.Length); + return mem.Span.Slice(0, size); } if (AudioData != null) { return new ReadOnlySpan<byte>(AudioData); } return ReadOnlySpan<byte>.Empty; }📝 Committable suggestion
🤖 Prompt for AI Agents