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
4 changes: 2 additions & 2 deletions csharp/src/AsterCache.Prefetch/ZoneStreamer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public sealed record StreamStats(long Bytes, long Requests, long Errors, double

public long BufferedBytes { get; init; }

/// 예열 완료 로그 줄에 붙이는 말 (스파스 파일을 버퍼드로 읽은 것이 없으면 빈 문자열).
public string BufferedNote() => BufferedBytes > 0 ? $", {BufferedBytes >> 20} MiB of sparse files by buffered read" : string.Empty;
/// 예열 완료 로그 줄에 붙이는 말 (스파스 파일을 버퍼드로 읽은 것이 MiB 로 0 이면 빈 문자열).
public string BufferedNote() => BufferedBytes >> 20 > 0 ? $", {BufferedBytes >> 20} MiB of sparse files by buffered read" : string.Empty;
}

/// ops 를 파일 캐시로 가져옵니다. 쓰기 없음. Python prefetch/reader.py read_ops (method read | pvm).
Expand Down
16 changes: 11 additions & 5 deletions csharp/src/AsterCache.Service/Prewarm.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ void Settle(StreamStats stats, bool shortFinish)
while (true)
{
streamed = StreamHot(warm, ct, zoneBytes, availableRamMib, memoryPartial is not null, Settle,
allowReorder: memoryPartial is null && !HasPartialState(game.Name), roomMib: roomMib ?? long.MaxValue, allowWhileExiting: trigger, why: why);
allowReorder: memoryPartial is null && !HasPartialState(game.Name), roomMib: roomMib ?? long.MaxValue, allowWhileExiting: trigger, why: why, windowMib: memoryPartial?.WarmedMib ?? 0);
if (fellBack || memoryPartial is not null || streamed is not { Complete: false, MemoryStop: true }
|| MemoryFallbackCapMib(game, finalReadMib, roomMib ?? long.MaxValue, ct) is not { } fallbackCap)
{
Expand Down Expand Up @@ -443,8 +443,9 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
// settle: 끝까지 읽었을 때 (또는 아래 메모리 한도로 마무리했을 때, 두 번째 인자 true) StreamEnded 알림보다 먼저 부름 (읽은 통계를 받아 부분 예열 상태를 확정하거나 지움).
// 창 읽기 (partial) 와 allowFinish 가 켜진 읽기 (순서를 바꾸지 않는 보충) 는 메모리 한도에 걸려도 취소하지 않고 읽은 데까지로 마무리해 (Complete 와 MemoryStop 이 함께 참, CancelReason 은 가용·예비량) 돌려줍니다.
// 틈을 메우는 계획 (partial 아님) 이 마무리하면 통계의 GapBytes > 0 이라 호출 쪽이 읽은 양을 보수적으로 세고 이어 읽을 위치는 남기지 않습니다. limitBytes: 건너뛴 앞부분을 포함해 이만큼까지만 읽음.
// windowMib: 읽는 창 (MiB) 의 크기, 틈 없는 창 읽기가 한도로 마무리했을 때 읽은 범위의 사다리를 만드는 데 씀 (0 이면 모름: 만들지 않음).
// skipBytes: 읽기 목록 앞쪽 이만큼은 이미 읽었으니 건너뜀 (끊긴 보충을 이어 읽음, 틈 메우기 없는 경로에서만). onTotal: 이번에 읽을 목록의 길이 (건너뛴 앞부분 제외).
private (StreamStats Stats, bool Complete, string? CancelReason, bool MemoryStop)? StreamHot(GameSettings game, CancellationToken ct, long? zoneBytes, long availableRamMib, bool partial = false, Action<StreamStats, bool>? settle = null, long skipBytes = 0, Action<long>? onTotal = null, bool allowReorder = true, long roomMib = long.MaxValue, bool allowWhileExiting = false, string? why = null, long limitBytes = long.MaxValue, bool allowFinish = false)
private (StreamStats Stats, bool Complete, string? CancelReason, bool MemoryStop)? StreamHot(GameSettings game, CancellationToken ct, long? zoneBytes, long availableRamMib, bool partial = false, Action<StreamStats, bool>? settle = null, long skipBytes = 0, Action<long>? onTotal = null, bool allowReorder = true, long roomMib = long.MaxValue, bool allowWhileExiting = false, string? why = null, long limitBytes = long.MaxValue, bool allowFinish = false, long windowMib = 0)
{
using DiskReadCoordinator.Lease? lease = _coordinator?.TryBegin(game.OriginRoot, "prewarm stream", ct, preserveForGameRoot: _auto.KeepStreamOnGameStart ? game.OriginRoot : null, preserveGameKey: game.Name, allowWhileExiting: allowWhileExiting);
if (_coordinator is not null && (lease is null || !lease.IsValid))
Expand Down Expand Up @@ -480,7 +481,8 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
// 읽기는 이 스레드 (묶음 점검 콜백도 같은 스레드) 에서 내므로 I/O 우선순위도 여기서 바꾸고 아래 finally 에서 되돌립니다.
var readYield = new ReadYield(_env.SetReadIoLow, PrewarmYield.StartsYielding(why, _env.UptimeMs()));
StreamRequeue? requeue = BeginRequeue(game, readYield.Exclude); // 읽는 묶음마다 곧바로 다시 만듦 (SOLAR 가 꺼져 있으면 null: 스트림은 지금과 같음)
double ExposureSeconds() => Math.Max(0, Stopwatch.GetElapsedTime(startedAt).TotalSeconds - (requeue?.Seconds ?? 0)); // 다시 만지기는 디스크 읽기가 아니므로 노출 시간에서 뺌
double protectedSeconds = 0; // 스트림 앞의 보호가 쓴 시간
double ExposureSeconds() => Math.Max(0, Stopwatch.GetElapsedTime(startedAt).TotalSeconds - (requeue?.Seconds ?? 0) - protectedSeconds); // 다시 만지기는 디스크 읽기가 아니므로 노출 시간에서 뺌
try
{
using CancellationTokenRegistration leaseReg = lease?.Token.Register(() =>
Expand All @@ -504,6 +506,9 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
long allocationBytes = allocationMib > (long.MaxValue >> 20) ? long.MaxValue : allocationMib << 20;
maxBytes = Math.Min(maxBytes, allocationBytes);
}
RequeueTally protectedOthers = ProtectOthers(game, stop.Token, allowWhileExiting, readYield); // 디스크를 읽기 전에 (보정 읽기도 읽기): 트리거 예열은 하지 않음
protectedSeconds = protectedOthers.ProtectedSeconds;
_requeued += protectedOthers;
// 트리거 예열은 보정 읽기를 하지 않음 (ORBIT 결정은 이미 쌓인 장치 구간만 쓰고 이번 스트림의 표본을 요구하지 않음)
if (!allowWhileExiting && _env.DeviceCalibration is not null && (_env.DeviceCalibrationEnabled?.Invoke() ?? true)) // 훅이 없거나 off 면 기준선과 같은 호출만 남김
{
Expand Down Expand Up @@ -552,9 +557,10 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
finalStats = r.Item1;
LogYield(game.Name, r.Item1.Bytes, lease);
streamSeconds = ExposureSeconds();
if (complete && r.Item1.Bytes > 0)
if (r.Item1.Bytes > 0 && (complete || (shortFinish && partial && r.Item1.GapBytes == 0 && windowMib > 0)))
{
LadderAfterStream(game, stop.Token); // 같은 디스크 대여 안에서: 게임 시작·대여 취소가 stop 으로 바로 멈춤. 한도로 마무리한 스트림은 건너뜀: 읽은 만큼의 앞부분은 창 크기가 달라 다시 계획한 목록과 같지 않아, 읽지 않은 구간을 만질 수 있음
// 같은 디스크 대여 안에서: 게임 시작·대여 취소가 stop 으로 바로 멈춤. 한도로 마무리한 스트림은 창 읽기·이어 읽기 (partial: 틈 메우기 예산 0, 스트림이 읽은 목록 = 같은 창으로 계획한 목록) 일 때만 읽은 창의 앞부분을 만짐: 예산이 있는 계획으로 읽은 전체 경로는 목록이 달라질 수 있어 건너뜀
LadderAfterStream(game, stop.Token, complete ? null : () => WindowLadderReads(game, windowMib, skipBytes + r.Item1.Bytes));
}

return shortFinish ? (r.Item1, true, fit!.Reason, true) : (r.Item1, complete, cancelDetail.First, cancelDetail.Memory);
Expand Down
73 changes: 70 additions & 3 deletions csharp/src/AsterCache.Service/Prewarm.Solar.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using AsterCache.Core;
using AsterCache.Core.Json;
using AsterCache.Core.Logging;
Expand Down Expand Up @@ -43,20 +44,59 @@ private void RequeueSkipped(GameSettings game, long skipBytes, StreamRequeue? re
}
}

// 핫 집합 스트림이 디스크를 읽기 시작하기 직전 (StreamHot, 디스크 대여 안): 표식이 있는 다른 게임들을 순위가 낮은 것부터 한 번 만집니다 (유지 틱의 묶음 새로 고침과 같은 함수, 대상에서 이 게임만 뺌).
// 그러면 이 스트림이 새로 읽는 페이지는 다른 게임이 아니라 캐시 (와 이 게임 자신의 오래된 페이지) 부터 밀어냅니다. 표본 점검과 게이트는 새로 고침과 같습니다.
// 트리거 예열 (allowWhileExiting) 은 게임 시작이 임박한 경로라 늦추지 않으려고 하지 않고, 스트림이 끝난 뒤 사다리가 다른 게임의 A 만 줄입니다 (Solar.Order).
// 만지는 데 쓴 시간은 스트림의 노출 시간·양보 시간에서 빼도록 돌려줍니다. 실패·취소돼도 스트림 결과는 같습니다.
private RequeueTally ProtectOthers(GameSettings game, CancellationToken stop, bool trigger, ReadYield readYield)
{
bool Halted() => _streamGameStarted || _streamPreempted || PreemptedByTriggerWait(game.Name);
if (trigger || !_solar.Enabled || _auto.KeepStreamOnGameStart || stop.IsCancellationRequested || Halted())
{
return default;
}

IReadOnlyList<GameSettings> others = _solar.Marked(Enumerable.Reverse(Warmed()).Where(g => !string.Equals(g.Name, game.Name, StringComparison.OrdinalIgnoreCase))); // Warmed 는 최근 순
if (others.Count == 0)
{
return default;
}

long startedAt = Stopwatch.GetTimestamp();
(int Games, long Bytes) touched = (0, 0);
try
{
touched = _rewarm.RefreshGroup(others, _auto, stop, Halted);
}
catch (OperationCanceledException)
{
// 스트림이 멈춤: 아무것도 하지 않음
}
catch (Exception e) when (e is not (OutOfMemoryException or StackOverflowException))
{
_log.Warn($"{game.Name}: protecting the other games skipped: {e.GetType().Name}"); // 예열 결과에는 영향을 주지 않음
}

double seconds = Stopwatch.GetElapsedTime(startedAt).TotalSeconds;
readYield.Exclude(seconds);
return new RequeueTally(0, 0, false, 0, touched.Games, touched.Bytes, seconds);
}

// 핫 집합 스트림이 끝까지 읽은 직후 (StreamHot, 디스크 대여 안): 그 게임이 지금까지 캐시에 올린 전체의 역순으로 캐시를 만져 중요한 것이 마지막에 밀려나게 합니다.
// game 은 스트림이 읽은 범위를 나타내는 사본 (메모리·상한으로 줄인 것 포함, 보충이면 이어 읽은 창) 이라 앞에서 읽은 부분과 보충분이 모두 대상입니다. 방금 읽었으므로 표본 점검은 생략합니다.
// reads: 메모리 한도로 마무리한 스트림은 읽은 창의 앞부분 (WindowLadderReads), 끝까지 읽었으면 null (읽은 범위 전체).
// 게임이 이미 도는 디스크이거나 keep_stream_on_game_start (게임이 떠도 스트림을 끊지 않는 실험 설정) 가 켜져 있으면 만지지 않습니다: 만지기도 캐시에 없으면 디스크 읽기라 게임을 느리게 할 수 있습니다.
private void LadderAfterStream(GameSettings game, CancellationToken stop)
private void LadderAfterStream(GameSettings game, CancellationToken stop, Func<IReadOnlyList<ReadOp>?>? reads = null)
{
if (!_solar.Enabled || _auto.KeepStreamOnGameStart || _streamGameStarted || stop.IsCancellationRequested || _streamPreempted || PreemptedByTriggerWait(game.Name) || DriveHasRunningGame(game)
|| LadderReads(game) is not { Count: > 0 } reads)
|| (reads is null ? LadderReads(game) : reads()) is not { Count: > 0 } ladder)
{
return;
}

try
{
_ = _solar.Order(game, reads, stop);
_ = _solar.Order(game, ladder, stop);
}
catch (Exception e) when (e is not (OperationCanceledException or OutOfMemoryException or StackOverflowException))
{
Expand Down Expand Up @@ -84,6 +124,26 @@ private void LadderAfterStream(GameSettings game, CancellationToken stop)
}
}

// 틈 없는 창 읽기가 읽은 범위의 사다리 대상 (공통 블록 + 창 목록의 앞 bytes 바이트): 창 (windowMib) 은 스트림이 읽은 것과 같은 크기로 계획하므로 읽지 않은 구간을 만지지 않습니다. 계획할 수 없으면 null.
private IReadOnlyList<ReadOp>? WindowLadderReads(GameSettings game, long windowMib, long bytes)
{
try
{
GameSettings window = game with { MaxStreamMib = (int)Math.Min(windowMib, int.MaxValue), MaxStreamSet = true };
if (_streamer.PlannedPrewarmReads(window) is not { Count: > 0 } planned || PrefetchStreamer.Head(planned, bytes) is not { Count: > 0 } read)
{
return null;
}

IReadOnlyList<ReadOp> common = _env.CommonFirstOps(window, 0, 0).Ops;
return common.Count == 0 ? read : [.. common, .. read];
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException or PlanRejectedException or PyValueException)
{
return null;
}
}

/// 지금까지 읽은 범위를 나타내는 게임: 부분 예열 (메모리·시작 상한) 이면 읽은 앞부분까지로 줄인 사본, 아니면 그대로.
internal GameSettings CoveredPart(GameSettings g)
{
Expand All @@ -93,6 +153,13 @@ internal GameSettings CoveredPart(GameSettings g)
}
}

// 이 게임 디스크의 읽기 대여를 지금 얻을 수 있는가 (얻으면 바로 놓음): 새로 고침이 대여 거절을 "표본 없음" 으로 보고 표식을 지우지 않게 먼저 물어봅니다.
internal bool DiskReadAllowed(GameSettings game, CancellationToken ct)
{
using DiskReadCoordinator.Lease? lease = _coordinator?.TryBegin(game.OriginRoot, "cache refresh gate", ct);
return _coordinator is null || lease is { IsValid: true };
}

// 디스크 대여 안에서 work 를 돌립니다 (거절·무효면 null): 게임이 시작돼 대여가 취소되면 work 의 토큰이 취소됩니다.
internal TouchEnd? UnderLease(GameSettings game, string purpose, CancellationToken ct, Func<CancellationToken, TouchEnd?> work)
{
Expand Down
18 changes: 2 additions & 16 deletions csharp/src/AsterCache.Service/Prewarm.Supplement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
{
NoteProgress(game, inputStamp, ActualMib(stats, p.HotMib, skipBytes), p.HotMib, skipBytes + stats.Bytes);
}
}, allowReorder: false, roomMib: allocatedMib ?? long.MaxValue, limitBytes: limitMib is { } limitBytesMib ? MibBytes(limitBytesMib) : long.MaxValue, allowFinish: true);
}, allowReorder: false, roomMib: allocatedMib ?? long.MaxValue, limitBytes: limitMib is { } limitBytesMib ? MibBytes(limitBytesMib) : long.MaxValue, allowFinish: true, windowMib: windowMib ?? p.HotMib);
if (fellBack || windowMib is not null || hot is not { Complete: false, MemoryStop: true } || !HeavyWorkAllowed(game)
|| MemoryFallbackCapMib(game, finalReadMib, budgetCap, ct) is not { } fallbackCap
|| fallbackCap <= Math.Max(p.WarmedMib, ActualMib(hot.Value.Stats, p.HotMib, skipBytes)))
Expand Down Expand Up @@ -881,21 +881,7 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
}
}

try
{
GameSettings window = game with { MaxStreamMib = (int)Math.Min(resume.WindowMib, int.MaxValue), MaxStreamSet = true };
if (_streamer.PlannedPrewarmReads(window) is not { Count: > 0 } planned || PrefetchStreamer.Head(planned, resume.Bytes) is not { Count: > 0 } read)
{
return null;
}

IReadOnlyList<ReadOp> common = _env.CommonFirstOps(window, 0, 0).Ops;
return common.Count == 0 ? read : [.. common, .. read];
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException or PlanRejectedException or PyValueException)
{
return null;
}
return WindowLadderReads(game, resume.WindowMib, resume.Bytes);
}

// 들어가는 범위가 읽은 양보다 의미 있게 크지 않아 보충을 시작하지 않는 상태: 읽은 양이 바뀔 때만 한 줄 (들어가는 범위는 가용 메모리에 따라 계속 흔들리므로 키에 넣지 않음).
Expand Down
Loading