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
2 changes: 1 addition & 1 deletion csharp/src/AsterCache.Service/Prewarm.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ private void CheckPrewarmBatch(GameSettings game, CancellationTokenSource stop,
if (r.Item1.Bytes > 0 && (complete || (shortFinish && partial && r.Item1.GapBytes == 0 && windowMib > 0)))
{
// 같은 디스크 대여 안에서: 게임 시작·대여 취소가 stop 으로 바로 멈춤. 한도로 마무리한 스트림은 창 읽기·이어 읽기 (partial: 틈 메우기 예산 0, 스트림이 읽은 목록 = 같은 창으로 계획한 목록) 일 때만 읽은 창의 앞부분을 만짐: 예산이 있는 계획으로 읽은 전체 경로는 목록이 달라질 수 있어 건너뜀
LadderAfterStream(game, stop.Token, complete ? null : () => WindowLadderReads(game, windowMib, skipBytes + r.Item1.Bytes));
LadderAfterStream(game, stop.Token, allowWhileExiting, readYield, 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
24 changes: 19 additions & 5 deletions csharp/src/AsterCache.Service/Prewarm.Solar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ private RequeueTally ProtectBetweenBatches(GameSettings game, CancellationToken
private IEnumerable<GameSettings> OtherGames(GameSettings game) => Enumerable.Reverse(Warmed()).Where(g => !string.Equals(g.Name, game.Name, StringComparison.OrdinalIgnoreCase));

// pick 이 고른 다른 게임들을 만지고 (Rewarmer.RefreshGroup) 쓴 시간을 스트림의 양보 시간에서 빼도록 알립니다. 고른 게임이 없으면 아무것도 하지 않습니다.
private RequeueTally TouchOthers(GameSettings game, CancellationToken stop, ReadYield readYield, Func<IReadOnlyList<GameSettings>> pick, long aheadLess = 0)
// touchedGames: 주면 끝까지 만진 게임 (만진 순서, 이름과 바이트) 을 채워 줍니다 (스트림 끝의 사다리가 묶음으로 다시 표식할 때 씀).
private RequeueTally TouchOthers(GameSettings game, CancellationToken stop, ReadYield readYield, Func<IReadOnlyList<GameSettings>> pick, long aheadLess = 0, List<(string Game, long Bytes)>? touchedGames = null)
{
long startedAt = Stopwatch.GetTimestamp();
(int Games, long Bytes, IReadOnlyList<string> Names) touched = (0, 0, []);
(int Games, long Bytes, IReadOnlyList<(string Game, long Bytes)> Touched) touched = (0, 0, []);
try
{
if (pick() is not { Count: > 0 } others)
Expand All @@ -106,14 +107,17 @@ private RequeueTally TouchOthers(GameSettings game, CancellationToken stop, Read
_protectedNames.Clear(); // 이번 예열 한 회의 첫 보호
}

return new RequeueTally(0, 0, false, 0, touched.Names.Count(_protectedNames.Add), touched.Bytes, seconds, touched.Games > 0 ? 1 : 0);
touchedGames?.AddRange(touched.Touched);
return new RequeueTally(0, 0, false, 0, touched.Touched.Count(t => _protectedNames.Add(t.Game)), touched.Bytes, seconds, touched.Games > 0 ? 1 : 0);
}

// 핫 집합 스트림이 끝까지 읽은 직후 (StreamHot, 디스크 대여 안): 그 게임이 지금까지 캐시에 올린 전체의 역순으로 캐시를 만져 중요한 것이 마지막에 밀려나게 합니다.
// game 은 스트림이 읽은 범위를 나타내는 사본 (메모리·상한으로 줄인 것 포함, 보충이면 이어 읽은 창) 이라 앞에서 읽은 부분과 보충분이 모두 대상입니다. 방금 읽었으므로 표본 점검은 생략합니다.
// reads: 메모리 한도로 마무리한 스트림은 읽은 창의 앞부분 (WindowLadderReads), 끝까지 읽었으면 null (읽은 범위 전체).
// 게임이 이미 도는 디스크이거나 keep_stream_on_game_start (게임이 떠도 스트림을 끊지 않는 실험 설정) 가 켜져 있으면 만지지 않습니다: 만지기도 캐시에 없으면 디스크 읽기라 게임을 느리게 할 수 있습니다.
private void LadderAfterStream(GameSettings game, CancellationToken stop, Func<IReadOnlyList<ReadOp>?>? reads = null)
// 사다리를 만들기 직전에 (트리거 예열은 제외) 표식이 있는 다른 게임을 한 번 만져 보호합니다 (스트림 앞의 보호와 같은 함수·게이트, "때가 됐는지"와 묶음 최소 간격은 보지 않음: 스트림 끝의 이 순서를 확정해야 A 가 맞음).
// 그러면 이 게임의 사다리는 다른 게임들 뒤에 오고, 둘을 만진 순서대로 한 묶음으로 다시 표식합니다 (Solar.Order). 쓴 시간은 완료 줄의 보호 꼬리에 합산하고 양보 시간에서 뺍니다.
private void LadderAfterStream(GameSettings game, CancellationToken stop, bool trigger, ReadYield readYield, Func<IReadOnlyList<ReadOp>?>? reads = null)
{
if (!_solar.Enabled || _auto.KeepStreamOnGameStart || _streamGameStarted || stop.IsCancellationRequested || _streamPreempted || PreemptedByTriggerWait(game.Name) || DriveHasRunningGame(game)
|| (reads is null ? LadderReads(game) : reads()) is not { Count: > 0 } ladder)
Expand All @@ -123,7 +127,17 @@ private void LadderAfterStream(GameSettings game, CancellationToken stop, Func<I

try
{
_ = _solar.Order(game, ladder, stop);
List<(string Game, long Bytes)> protectedFirst = [];
if (!trigger && CanProtectOthers(game, stop))
{
_requeued += TouchOthers(game, stop, readYield, () => _solar.Marked(OtherGames(game)), touchedGames: protectedFirst);
if (ProtectionHalted(game) || stop.IsCancellationRequested)
{
return; // 보호하는 사이 게임이 시작했거나 선점됨: 진입할 때 막힌 것처럼 사다리를 시작하지 않음
}
}

_ = _solar.Order(game, ladder, stop, protectedFirst: protectedFirst);
}
catch (Exception e) when (e is not (OperationCanceledException or OutOfMemoryException or StackOverflowException))
{
Expand Down
10 changes: 5 additions & 5 deletions csharp/src/AsterCache.Service/Rewarm.Ladder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace AsterCache.Service;
/// 사다리를 새로 고치는 일 자체 (메모리 안에서만 만지기) 는 자동 모드·rewarm_minutes 와 무관하게 memory.standby_ladder 로만 켜고 끕니다.
public sealed partial class Rewarmer
{
private readonly object _groupLock = new(); // 묶음 새로 고침 (유지 틱) 과 스트림 앞의 보호 (스트림 스레드) 가 같은 게임을 동시에 만지지 않게, 기다리지 않고 못 잡으면 그 회는 건너뜀
private readonly object _groupLock = new(); // 묶음 새로 고침 (유지 틱) 과 스트림의 보호 (스트림 스레드: 앞·도중·사다리 직전) 가 같은 게임을 동시에 만지지 않게, 기다리지 않고 못 잡으면 그 회는 건너뜀

/// 유지 틱: 대기 목록을 한 번 읽어 (표식이 있을 때만) 표식이 있는 게임 중 하나라도 새로 고칠 때가 됐으면 묶음 전체를 순위가 낮은 것부터 (가장 최근에 예열한 게임이 마지막) 다시 만집니다.
public void LadderTick(AutoSettings auto, CancellationToken ct = default)
Expand Down Expand Up @@ -43,10 +43,10 @@ private bool Touchable(GameSettings g, DateTimeOffset now, AutoSettings auto, Wa
!_prewarm.Games.Any(x => _prewarm.Relocating(x.Name)) && !_prewarm.RewarmAlreadyCovered(g) && ReadBlock(g, now, auto, budget, ignoreDriveLoad: true) is null && _prewarm.DiskReadAllowed(g, ct);

/// order (순위가 낮은 것부터) 의 표식이 있는 게임을 차례로 새로 고치고 (표본 점검 → warm 이면 만지기), 만진 게임들의 표식을 같은 기준으로 다시 씁니다 (Solar.Regroup).
/// 유지 틱의 묶음 새로 고침과 스트림 앞의 보호가 같은 함수를 씁니다. 게이트에 막힌 게임은 건너뛰고 나머지는 계속하며, halted 가 참이거나 취소·재배치가 시작되면 거기서 멈춥니다.
/// 유지 틱의 묶음 새로 고침과 스트림의 보호 (앞·도중·사다리 직전) 네 호출부가 같은 함수를 씁니다. 게이트에 막힌 게임은 건너뛰고 나머지는 계속하며, halted 가 참이거나 취소·재배치가 시작되면 거기서 멈춥니다.
/// aheadLess: 스트림 도중의 보호가 넘기는, 진행 중인 스트림의 몫 (Solar.Regroup 참고), 그 밖에는 0.
/// 돌려주는 값: 끝까지 만진 게임 수와 만진 바이트와 그 게임 이름.
internal (int Games, long Bytes, IReadOnlyList<string> Names) RefreshGroup(IReadOnlyList<GameSettings> order, AutoSettings auto, CancellationToken ct, Func<bool> halted, long aheadLess = 0)
/// 돌려주는 값: 끝까지 만진 게임 수와 만진 바이트와 그 게임들 (만진 순서, 이름과 바이트).
internal (int Games, long Bytes, IReadOnlyList<(string Game, long Bytes)> Touched) RefreshGroup(IReadOnlyList<GameSettings> order, AutoSettings auto, CancellationToken ct, Func<bool> halted, long aheadLess = 0)
{
if (!Monitor.TryEnter(_groupLock))
{
Expand Down Expand Up @@ -77,7 +77,7 @@ private bool Touchable(GameSettings g, DateTimeOffset now, AutoSettings auto, Wa
Monitor.Exit(_groupLock);
}

return (touched.Count, touched.Sum(t => t.Bytes), [.. touched.Select(t => t.Game)]);
return (touched.Count, touched.Sum(t => t.Bytes), [.. touched.Select(t => (t.Game, t.Bytes))]);
}

// 새로 고치기 전에 기존 재예열 점검의 표본 (warm/cold 판정) 을 한 번 거칩니다: warm 일 때만 만집니다.
Expand Down
28 changes: 20 additions & 8 deletions csharp/src/AsterCache.Service/Solar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ public void Prune(IReadOnlySet<string> live)

/// 읽은 목록을 예열 순서의 역순으로 만지고 표식을 남깁니다 (예열 직후, 표식 없는 게임의 첫 사다리). 꺼져 있거나 만질 것이 없으면 null.
/// made: 표식을 남겼을 때 만진 바이트를 알림 (서비스 시작 직후 여러 게임의 사다리가 Regroup 에 넘길 값).
public TouchEnd? Order(GameSettings game, IReadOnlyList<ReadOp> reads, CancellationToken ct, Action<long>? made = null)
/// protectedFirst: null 이면 스트림 끝의 사다리가 아님 (기존 규칙). 비어 있으면 스트림 끝의 사다리인데 직전에 보호한 게임이 없음: 이전 표식이 없는 게임이 만진 전체를 다른 게임의 앞에 있는 양에서 줄임.
/// 비어 있지 않으면 스트림 끝이고 직전에 보호로 만진 다른 게임 (만진 순서, 바이트): 이 게임과 함께 한 묶음으로 다시 표식합니다.
public TouchEnd? Order(GameSettings game, IReadOnlyList<ReadOp> reads, CancellationToken ct, Action<long>? made = null, IReadOnlyList<(string Game, long Bytes)>? protectedFirst = null)
{
if (!Enabled)
{
Expand Down Expand Up @@ -113,7 +115,16 @@ public void Prune(IReadOnlySet<string> live)
LadderMark? previous = MarkOf(game.Name); // SetMark 가 덮어쓰기 전의 표식
SetMark(game.Name, ladder, r);
made?.Invoke(r.Bytes);
ReduceOthers([game.Name], [(previous, r.Bytes)]); // 이 게임이 만진 페이지가 다른 게임의 뒤로 감
if (protectedFirst is { Count: > 0 })
{
// 보호한 게임들은 이미 만진 만큼 다른 표식에서 줄였으므로 (alreadyReduced) 이 게임의 몫만 더 줄임. 만진 순서 그대로 한 묶음으로 A 를 실제 순서에 맞게 다시 계산
Regroup([.. protectedFirst.Select(p => (p.Game, p.Bytes, (LadderMark?)null)), (game.Name, r.Bytes, previous)], alreadyReduced: protectedFirst.Count, reduceWithoutPrevious: true);
}
else
{
ReduceOthers([game.Name], [(previous, r.Bytes)], protectedFirst is not null); // 이 게임이 만진 페이지가 다른 게임의 뒤로 감
}

log.Info($"{game.Name}: ladder ordered {r.Bytes >> 20} MiB in {r.Seconds:F1} s{Errors(r)}{Sparse(r)}");
break;
case TouchEnd.Canceled:
Expand Down Expand Up @@ -213,7 +224,8 @@ public IReadOnlyList<GameSettings> DueGroup(IEnumerable<GameSettings> games, Dat
/// 묶음을 만진 순서대로 (touched: 게임, 만진 바이트, 만지기 전의 표식, 가장 오래된 것이 먼저) 표식을 같은 기준 시점 (재사용 누계·대기 바이트) 으로 다시 쓰고,
/// 이번에 만지지 않은 표식은 앞에 있는 양을 SolarPolicy.AheadAfterOthersTouched 로 줄입니다. 대기 목록을 못 읽으면 표식은 그대로 두고 (시간 기준) 줄이기만 합니다.
/// aheadLess: 진행 중인 스트림이 이미 읽은 양 중 끝의 사다리가 줄여 주지 않을 몫 (스트림 도중의 보호), 캐시 대기 바이트에서 빼고 계산합니다.
public void Regroup(IReadOnlyList<(string Game, long Bytes, LadderMark? Previous)> touched, long aheadLess = 0)
/// alreadyReduced: touched 의 앞 몇 개는 이미 다른 표식에서 줄였음 (스트림 끝의 묶음: 보호가 먼저 줄임). reduceWithoutPrevious: SolarPolicy.AheadAfterOthersTouched 의 withoutPreviousAll.
public void Regroup(IReadOnlyList<(string Game, long Bytes, LadderMark? Previous)> touched, long aheadLess = 0, int alreadyReduced = 0, bool reduceWithoutPrevious = false)
{
if (touched.Count == 0)
{
Expand All @@ -233,7 +245,7 @@ public void Regroup(IReadOnlyList<(string Game, long Bytes, LadderMark? Previous
}
}

ReduceOthersLocked([.. touched.Select(t => t.Game)], [.. touched.Select(t => (t.Previous, t.Bytes))]);
ReduceOthersLocked([.. touched.Select(t => t.Game)], [.. touched.Skip(alreadyReduced).Select(t => (t.Previous, t.Bytes))], reduceWithoutPrevious);
_groupAt = at;
}
}
Expand Down Expand Up @@ -264,20 +276,20 @@ public void Regroup(IReadOnlyList<(string Game, long Bytes, LadderMark? Previous
}

// except 가 아닌 표식의 A 를 줄입니다 (touched: 그 게임들이 만지기 전의 표식과 만진 바이트).
private void ReduceOthers(IEnumerable<string> except, IReadOnlyList<(LadderMark? Previous, long Bytes)> touched)
private void ReduceOthers(IEnumerable<string> except, IReadOnlyList<(LadderMark? Previous, long Bytes)> touched, bool withoutPreviousAll = false)
{
lock (_lock)
{
ReduceOthersLocked(except, touched);
ReduceOthersLocked(except, touched, withoutPreviousAll);
}
}

private void ReduceOthersLocked(IEnumerable<string> except, IReadOnlyList<(LadderMark? Previous, long Bytes)> touched)
private void ReduceOthersLocked(IEnumerable<string> except, IReadOnlyList<(LadderMark? Previous, long Bytes)> touched, bool withoutPreviousAll = false)
{
HashSet<string> skip = new(except, StringComparer.OrdinalIgnoreCase);
foreach ((string game, LadderMark mark) in _marks.Where(m => !skip.Contains(m.Key)).Select(m => (m.Key, m.Value)).ToList())
{
_marks[game] = mark with { AheadBytes = SolarPolicy.AheadAfterOthersTouched(mark, touched.Select(t => (t.Previous, t.Bytes))) };
_marks[game] = mark with { AheadBytes = SolarPolicy.AheadAfterOthersTouched(mark, touched.Select(t => (t.Previous, t.Bytes)), withoutPreviousAll) };
}
}

Expand Down
11 changes: 7 additions & 4 deletions csharp/src/AsterCache.Service/SolarPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ internal static class SolarPolicy
/// 다른 게임들이 방금 만진 뒤 우리 앞에 있는 양: 그 게임 X 의 이전 표식이 우리 표식보다 먼저 만들어졌을 때만 (그때만 X 의 페이지가 우리 앞에 있었음) min(X 의 이전 표식이 만진 바이트, X 가 이번에 만진 바이트) 만큼 줄어듦 (0 미만이면 0).
/// X 에 이전 표식이 없었거나 (새로 예열된 게임) 우리 표식보다 나중이면 줄이지 않습니다. 스트림이 디스크에서 새로 읽은 양은 재사용 누계에 이미 잡히므로 이 규칙에서 자연히 빠지고, 스파스로 건너뛴 양은 만진 바이트에 없습니다.
/// 묶음 새로 고침·스트림 뒤 사다리·스트림 앞의 보호가 같은 규칙을 씁니다 (묶음에서 새 표식을 받는 게임은 GroupAhead 로 다시 계산하므로 대상이 아님).
public static long AheadAfterOthersTouched(LadderMark mine, IEnumerable<(LadderMark? Previous, long TouchedBytes)> others) =>
Ahead(mine.AheadBytes, others.Sum(o => o.Previous is { } previous && previous.At < mine.At ? Math.Min(previous.TouchedBytes, o.TouchedBytes) : 0));
/// withoutPreviousAll: 이전 표식이 없는 X 가 만진 바이트 전체를 줄입니다 (스트림 끝의 사다리용): 재예열·보충 스트림의 게임은 표식이 이미 지워져 있어 위 규칙대로면 0 이 되는데, 실제로는 그 게임이 캐시에 가진 페이지가 우리 앞에서 뒤로 가기 때문.
/// 얼마가 그런 페이지인지 알 수 없어 만진 전체를 줄입니다 (과하면 새로 고침이 한 번 일찍 올 뿐이고 덜 줄이면 캐시를 잃음).
public static long AheadAfterOthersTouched(LadderMark mine, IEnumerable<(LadderMark? Previous, long TouchedBytes)> others, bool withoutPreviousAll = false) =>
Ahead(mine.AheadBytes, others.Sum(o => o.Previous is { } previous ? (previous.At < mine.At ? Math.Min(previous.TouchedBytes, o.TouchedBytes) : 0) : withoutPreviousAll ? o.TouchedBytes : 0));

/// 스트림 도중의 보호가 새 표식의 A 를 계산할 때 캐시 대기 바이트에서 뺄 양: 스트림이 그때까지 읽은 양은 끝의 사다리에서 다시 만져져 보호한 게임들의 뒤로 가지만,
/// 끝의 사다리는 스트림 게임의 이전 표식이 있을 때만 min(이전 표식이 만진 양, 이번 양) 을 줄이므로 (AheadAfterOthersTouched) 그 몫을 빼 이중으로 줄지 않게 합니다.
public static long StreamAheadLess(long streamReadBytes, long priorTouchedBytes) => Math.Max(0, streamReadBytes - priorTouchedBytes);
/// 끝의 사다리는 스트림 게임의 이전 표식이 있으면 min(이전 표식이 만진 양, 이번 양) 만 줄이므로 (AheadAfterOthersTouched) 그 몫을 뺀 나머지를 미리 뺍니다.
/// 이전 표식이 없으면 (priorTouchedBytes 0) 끝의 사다리가 만진 전체를 줄이므로 (withoutPreviousAll) 여기서는 빼지 않습니다: 빼면 끝에서 건너뛴 게임이 두 번 줄고, 사다리 없이 끝나도 스트림 페이지는 그 게임 앞에 그대로 있어 안 빼는 쪽이 실제와 맞습니다.
public static long StreamAheadLess(long streamReadBytes, long priorTouchedBytes) => priorTouchedBytes > 0 ? Math.Max(0, streamReadBytes - priorTouchedBytes) : 0;

/// 묶음을 만진 순서 (가장 오래된 것이 먼저) 대로 각 게임의 앞에 있는 양: g_i 의 A = max(0, C - (g_i 와 그 뒤에 만진 게임들이 만진 합)), C 는 만진 직후 캐시 대기 바이트.
public static long[] GroupAhead(long cacheStandbyBytes, IReadOnlyList<long> touchedBytes)
Expand Down
Loading