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
35 changes: 29 additions & 6 deletions csharp/src/AsterCache.Service/Prewarm.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ void Settle(StreamStats stats, bool shortFinish)
(StreamStats Stats, bool Complete, string? CancelReason, bool MemoryStop)? streamed;
bool fellBack = false; // 전체 경로가 메모리로 멈추면 창으로 줄여 한 번만 다시 읽음
_yieldedSeconds = 0; // StreamHot 이 끝날 때마다 더함 (done 줄에 한 번 보임)
_requeued = default;
while (true)
{
streamed = StreamHot(warm, ct, zoneBytes, availableRamMib, memoryPartial is not null, Settle,
Expand Down Expand Up @@ -324,7 +325,7 @@ void Settle(StreamStats stats, bool shortFinish)

double mbps = s.Seconds > 0 ? s.Bytes / 1048576.0 / s.Seconds : 0;
string kind = memoryPartial is { } part ? $"partial {ActualMib(s, part.WarmedMib)}/{part.HotMib} MiB, the rest does not fit in memory: " : string.Empty;
_log.Info($"{game.Name}: prewarm done ({why}): {kind}{s.Bytes >> 20} MiB requested, {s.GapBytes / 1048576.0:F2} MiB gaps filled in {s.Seconds:F2} s = {mbps:F0} hint MB/s, {s.Requests} requests, {s.Errors} errors, room {(roomMib is { } roomLimit ? roomLimit + " MiB" : "unlimited")}, available {_env.AvailableRamMib()} MiB{(_yieldedSeconds > 0 ? $", yielded {_yieldedSeconds:F1} s" : string.Empty)}");
_log.Info($"{game.Name}: prewarm done ({why}): {kind}{s.Bytes >> 20} MiB requested, {s.GapBytes / 1048576.0:F2} MiB gaps filled in {s.Seconds:F2} s = {mbps:F0} hint MB/s, {s.Requests} requests, {s.Errors} errors, room {(roomMib is { } roomLimit ? roomLimit + " MiB" : "unlimited")}, available {_env.AvailableRamMib()} MiB{(_yieldedSeconds > 0 ? $", yielded {_yieldedSeconds:F1} s" : string.Empty)}{_requeued.Note()}");
_log.Info($"{game.Name}: prewarm phases ({why}): {run.Summary()}");
PrewarmResult done = Record(game, warmedAt, new PrewarmResult(game.Name, true, null, s, warmedAt), mbps);
WarmMeasured?.Invoke(game, mbps);
Expand Down Expand Up @@ -478,6 +479,8 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
double? streamSeconds = null; // 사다리 시간이 노출 시간에 들어가지 않게 스트림이 끝난 때의 값
// 읽기는 이 스레드 (묶음 점검 콜백도 같은 스레드) 에서 내므로 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)); // 다시 만지기는 디스크 읽기가 아니므로 노출 시간에서 뺌
try
{
using CancellationTokenRegistration leaseReg = lease?.Token.Register(() =>
Expand Down Expand Up @@ -512,9 +515,10 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
}

calibration.Token.ThrowIfCancellationRequested();
RequeueSkipped(game, skipBytes, requeue, stop.Token); // 이어 읽기: 꼬리를 읽기 전에 건너뛴 앞부분을 한 번 만짐
using (PrewarmRun.Time(PrewarmPhase.Common))
{
WarmCommonFirst(game, stop, maxBytes, skipBytes, lease, cancelDetail, ct, readYield, fit);
WarmCommonFirst(game, stop, maxBytes, skipBytes, lease, cancelDetail, ct, readYield, fit, requeue);
}

calibration.Token.ThrowIfCancellationRequested();
Expand All @@ -534,7 +538,7 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
Progress?.Invoke(game, done >> 20, (total + (1 << 20) - 1) >> 20);
}
}, maxBytes, skipBytes, () => !stop.IsCancellationRequested && !(fit?.Finished ?? false) && (lease?.IsValid ?? true), allowReorder,
(stage, bytes, isData) => lease?.NoteReadIssue(stage, bytes, isData));
(stage, bytes, isData) => lease?.NoteReadIssue(stage, bytes, isData), requeue is null ? null : batch => requeue.Touch(batch, stop.Token));
// 읽은 것을 버리지 않아도 될 때만 (오류·순서 바꿈 없음) 마무리로 남기고, 아니면 기존 메모리 취소와 같게 둠. 틈을 메운 계획은 읽은 곳이 앞쪽과 같다고 보장할 수 없어도 마무리하되 호출 쪽이 보수적으로 셈 (GapBytes).
bool clean = r.Item1.Bytes > 0 && r.Item1.Errors == 0 && !_streamer.WasPrewarmReordered(game);
bool limited = fit is { Finished: true } && !stop.IsCancellationRequested && (lease?.IsValid ?? true);
Expand All @@ -547,7 +551,7 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
complete = r.Item2 && !stop.IsCancellationRequested && !limited;
finalStats = r.Item1;
LogYield(game.Name, r.Item1.Bytes, lease);
streamSeconds = Stopwatch.GetElapsedTime(startedAt).TotalSeconds;
streamSeconds = ExposureSeconds();
if (complete && r.Item1.Bytes > 0)
{
LadderAfterStream(game, stop.Token); // 같은 디스크 대여 안에서: 게임 시작·대여 취소가 stop 으로 바로 멈춤. 한도로 마무리한 스트림은 건너뜀: 읽은 만큼의 앞부분은 창 크기가 달라 다시 계획한 목록과 같지 않아, 읽지 않은 구간을 만질 수 있음
Expand All @@ -566,13 +570,14 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
{
FinishYield(readYield, game.Name); // 스레드 풀 스레드에 낮은 우선순위가 남지 않게 (정상·취소·예외 모두)
_yieldedSeconds += readYield.Seconds;
_requeued += requeue?.Tally ?? default;
bool interrupted = !complete && _streamGameStarted;
_calibrationStop = null;
(_streaming, _streamStop, _streamCancelDetail) = (null, null, null);
// shortcut: 스트림 PDH 는 순차 탐침과 비교할 수 없어 교차 확인을 배선하지 않음, 실측 이탈이 확인되면 끝까지 읽은 스트림의 실제 읽기 바이트/시간으로 다시 연결
try
{
_env.OrbitExposure?.Invoke(game, streamSeconds ?? Stopwatch.GetElapsedTime(startedAt).TotalSeconds, interrupted);
_env.OrbitExposure?.Invoke(game, streamSeconds ?? ExposureSeconds(), interrupted);
}
catch (Exception e) when (e is not (OutOfMemoryException or StackOverflowException))
{
Expand Down Expand Up @@ -626,7 +631,7 @@ private void CalibrateDevice(GameSettings game, IReadOnlyList<ReadOp> ops, strin
// 디스크 대여·중단 신호는 StreamHot 의 것을 그대로 씁니다. 실패하면 건너뛰고 스트림이 같은 오류를 판단합니다.
// 최종 예약 검사는 common 을 읽기 전에 이번 스트림 전체 양으로 합니다 (거절이면 common 도 읽지 않음).
private void WarmCommonFirst(GameSettings game, CancellationTokenSource stop, long maxBytes, long skipBytes, DiskReadCoordinator.Lease? lease,
PrewarmCancelDetail cancelDetail, CancellationToken callerToken, ReadYield? readYield = null, MemoryFit? fit = null)
PrewarmCancelDetail cancelDetail, CancellationToken callerToken, ReadYield? readYield = null, MemoryFit? fit = null, StreamRequeue? requeue = null)
{
try
{
Expand All @@ -652,6 +657,16 @@ private void WarmCommonFirst(GameSettings game, CancellationTokenSource stop, lo
}

long total = ops.Sum(o => o.Length);
long requeued = 0; // 읽은 공통 블록 중 이미 다시 만진 바이트: 묶음 경계 (읽은 양 done) 마다 새로 읽은 만큼만 읽은 순서대로 만짐
void Requeue(long readBytes)
{
if (requeue is not null && readBytes > requeued)
{
requeue.Touch(PrefetchStreamer.Tail(PrefetchStreamer.Head(ops, readBytes), requeued), stop.Token);
requeued = readBytes;
}
}

Action<long, long> progress = (done, planned) =>
{
CheckPrewarmBatch(game, stop, done, planned, detail: cancelDetail, callerToken: callerToken, leaseToken: lease?.Token ?? default, yield: readYield, preemptible: true);
Expand All @@ -660,6 +675,8 @@ private void WarmCommonFirst(GameSettings game, CancellationTokenSource stop, lo
cancelDetail.Set("disk read lease revoked");
stop.Cancel();
}

Requeue(done);
};
(StreamStats Stats, bool Complete) read;
if (_env.ColdReadWithIssue is { } withIssue)
Expand All @@ -676,6 +693,12 @@ private void WarmCommonFirst(GameSettings game, CancellationTokenSource stop, lo
lease?.NoteReadIssue("common-first read", total, true);
read = _env.ColdRead(ops, stop.Token);
}

if (read.Complete)
{
Requeue(total); // 묶음 진행을 알리지 않는 읽기 경로가 남긴 나머지
}

_log.Info($"{game.Name}: common-first prewarm {read.Stats.Bytes >> 20}/{total >> 20} MiB" + (read.Complete ? string.Empty : " (stopped)"));
}
catch (OperationCanceledException) when (stop.IsCancellationRequested)
Expand Down
29 changes: 29 additions & 0 deletions csharp/src/AsterCache.Service/Prewarm.Solar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,35 @@ public sealed partial class Prewarmer

internal Solar Solar => _solar;

private RequeueTally _requeued; // 핫 집합 스트림이 읽는 묶음을 다시 만든 양 (Run·보충이 예열 한 회 동안 모아 done 줄에 씀), _run 아래에서만 씀

// 핫 집합 스트림 한 번이 읽는 묶음마다 그 범위를 곧바로 다시 만지는 도우미 (StreamHot 이 만듦). 꺼져 있거나 keep_stream_on_game_start (게임이 떠도 스트림이 계속 읽음, 만지기도 디스크 읽기가 될 수 있음) 면 null.
// 만지기는 스트림과 같은 스레드·같은 대여·같은 중지 토큰 아래에서만 돌고, 게임 시작·트리거 선점 표시가 서면 건너뜁니다 (묶음 점검이 그 사유로 스트림을 멈추기 전의 틈도 막음).
private StreamRequeue? BeginRequeue(GameSettings game, Action<double>? onTouched = null) =>
_solar.Enabled && !_auto.KeepStreamOnGameStart ? new StreamRequeue(_env.TouchRanges, _log, game.Name, () => _streamGameStarted || _streamPreempted, onTouched) : null;

// 이어 읽기 (skipBytes > 0) 는 이전 회가 읽은 앞부분 (공통 블록은 목록의 부분집합이라 그 안에 있음) 을 읽지 않으므로, 그대로 두면 그 부분이 이 게임에서 가장 오래된 페이지로 남습니다.
// 꼬리를 읽기 전에 건너뛴 앞부분을 목록 순서대로 한 번 만집니다. 이어 읽기는 틈 메우기 없는 창 읽기라 계획한 목록 (PlannedPrewarmReads) 이 스트림의 목록과 같습니다. 계획할 수 없으면 건너뜁니다.
private void RequeueSkipped(GameSettings game, long skipBytes, StreamRequeue? requeue, CancellationToken stop)
{
if (requeue is null || skipBytes <= 0 || stop.IsCancellationRequested || DriveHasRunningGame(game))
{
return;
}

try
{
if (_streamer.PlannedPrewarmReads(game) is { Count: > 0 } planned)
{
requeue.TouchSkipped(PrefetchStreamer.Head(planned, skipBytes), stop);
}
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException or PlanRejectedException or PyValueException)
{
// 계획할 수 없음: 건너뛴 앞부분은 스트림 끝의 사다리가 처리
}
}

// 핫 집합 스트림이 끝까지 읽은 직후 (StreamHot, 디스크 대여 안): 그 게임이 지금까지 캐시에 올린 전체의 역순으로 캐시를 만져 중요한 것이 마지막에 밀려나게 합니다.
// game 은 스트림이 읽은 범위를 나타내는 사본 (메모리·상한으로 줄인 것 포함, 보충이면 이어 읽은 창) 이라 앞에서 읽은 부분과 보충분이 모두 대상입니다. 방금 읽었으므로 표본 점검은 생략합니다.
// 게임이 이미 도는 디스크이거나 keep_stream_on_game_start (게임이 떠도 스트림을 끊지 않는 실험 설정) 가 켜져 있으면 만지지 않습니다: 만지기도 캐시에 없으면 디스크 읽기라 게임을 느리게 할 수 있습니다.
Expand Down
9 changes: 5 additions & 4 deletions csharp/src/AsterCache.Service/Prewarm.Supplement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
_log.Info($"{game.Name}: supplement prewarm of the rest of the hot set ({p.WarmedMib} of {p.HotMib} MiB read{(windowMib is { } w ? $", reading the first {w} MiB" : string.Empty)}{(skipBytes > 0 ? $", continuing after {skipBytes >> 20} MiB" : string.Empty)})");
bool fellBack = false; // 전체 경로가 메모리로 멈추면 창으로 줄여 한 번만 다시 읽음
_yieldedSeconds = 0; // StreamHot 이 끝날 때마다 더함 (done 줄에 한 번 보임)
_requeued = default;
while (true)
{
hot = StreamHot(warm, ct, zoneBytes, avail, windowMib is not null || skipBytes > 0, skipBytes: skipBytes, onTotal: total => tailTotal = total, settle: (stats, shortFinish) =>
Expand Down Expand Up @@ -797,7 +798,7 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
if (requestListEnded)
{
DeferSupplement(game, plannedCapMib, Math.Max(progressBefore, progressAfter), inputStamp, r.Stats, planEnded: true, skipBytes + plannedListBytes);
_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors), the plan lists {MibText(skipBytes + plannedListBytes)} within {readCapMib} MiB");
_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors){_requeued.Note()}, the plan lists {MibText(skipBytes + plannedListBytes)} within {readCapMib} MiB");
return true;
}

Expand All @@ -808,7 +809,7 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
DeferSupplement(game, plannedCapMib, progressBefore, inputStamp, r.Stats, planEnded: false, skipBytes + plannedListBytes);
}

_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors), the partial window remains pending");
_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors){_requeued.Note()}, the partial window remains pending");
return true;
}

Expand All @@ -819,13 +820,13 @@ internal bool Supplement(GameSettings game, CancellationToken ct, long? allocate
DeferSupplement(game, plannedCapMib, progressBefore, inputStamp, r.Stats, planEnded: false, skipBytes + plannedListBytes);
}

_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors), the hot set remains partial");
_log.Info($"{game.Name}: supplement prewarm read the first {MibText(progressAfter)} of {p.HotMib} MiB in {r.Stats.Seconds:F2} s ({MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.Errors} errors){_requeued.Note()}, the hot set remains partial");
return true;
}

ClearPartial(game.Name, p.HotMib);
CompletePendingWarmSupplement(game, r.Stats);
_log.Info($"{game.Name}: supplement prewarm done: {MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.GapBytes / 1048576.0:F2} MiB gaps filled in {r.Stats.Seconds:F2} s, {r.Stats.Errors} errors{(_yieldedSeconds > 0 ? $", yielded {_yieldedSeconds:F1} s" : string.Empty)}");
_log.Info($"{game.Name}: supplement prewarm done: {MibText(r.Stats.Bytes)} accepted of {MibText(r.Stats.RequestedBytes)} requested, {r.Stats.GapBytes / 1048576.0:F2} MiB gaps filled in {r.Stats.Seconds:F2} s, {r.Stats.Errors} errors{(_yieldedSeconds > 0 ? $", yielded {_yieldedSeconds:F1} s" : string.Empty)}{_requeued.Note()}");
return true;
}
finally
Expand Down
14 changes: 12 additions & 2 deletions csharp/src/AsterCache.Service/PrewarmYield.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,22 @@ internal sealed class ReadYield(Func<bool, bool> setLow, bool startYielding)
private bool _everYielded;
private DateTimeOffset _since;
private TimeSpan _total;
private TimeSpan _excluded;

/// 양보로 시작했고 첫 관측을 아직 하지 않음 (첫 묶음 직전에 한 번 관측하고 내림).
public bool FirstObservationPending { get; set; } = startYielding;

/// 낮은 우선순위로 읽은 시간 (초).
public double Seconds => _total.TotalSeconds;
/// 낮은 우선순위로 읽은 시간 (초). 디스크를 읽지 않은 일 (다시 만지기) 에 쓴 시간은 뺍니다.
public double Seconds => Math.Max(0, (_total - _excluded).TotalSeconds);

/// 낮은 우선순위인 동안 디스크를 읽지 않는 일에 seconds 를 썼음을 알려 양보 시간에서 뺍니다.
public void Exclude(double seconds)
{
if (_low)
{
_excluded += TimeSpan.FromSeconds(seconds);
}
}

/// 상태를 바꿉니다 (low: 낮음, 아니면 보통). 바꾸지 못하면 (setLow 실패) 상태는 그대로이며 다음 관측 때 다시 시도합니다.
/// 이 스트림에서 처음 낮음으로 바뀐 때만 true (로그 한 줄용).
Expand Down
Loading