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
19 changes: 13 additions & 6 deletions csharp/src/AsterCache.Windows/StandbyTouch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public static unsafe class StandbyTouch
/// 디스크에서 읽힌 것으로 본 양이 이 값을 넘으면 멈춥니다.
public const long DiskReadStopBytes = 32L << 20;

/// 뷰를 만지는 데 걸린 시간 동안 디스크가 읽을 수 있다고 보는 양 (초당): 256 MiB/s. 이 서비스가 예열하는 대상은 HDD 의 게임이라 연속 읽기의 넉넉한 상한이고, 더 빠른 장치에서는 실제 읽기가 덜 세어질 뿐 읽기 자체가 빨라 해가 작습니다.
// 페이지 읽기 누계는 시스템 전체 값이라 다른 프로세스의 몰림 (같은 PC 에서 0.25 초에 3만~8만 쪽) 이 뷰를 만지는 순간과 겹치면 뷰 전체가 디스크 읽기로 보이기 때문에, 메모리에서 20 ms 에 끝난 뷰는 이 상한으로 몇 MiB 까지만 셉니다.
public const long MaxDiskBytesPerSecond = 256L << 20;

/// 페이지 읽기 누계를 읽을 수 없을 때만 쓰는 뷰 단위 시간 기준: 64 MiB 당 이 시간 (ms) 에 비례한 시간을 넘으면 그 뷰는 디스크에서 읽힌 것으로 봅니다 (캐시는 64 MiB 에 약 30 ms, HDD 는 250 ms 이상).
public const double ViewSlowMsPer64Mib = 256.0;

Expand Down Expand Up @@ -69,15 +73,17 @@ private sealed class Tally
public long Skipped;
public required Func<uint?> PageReads;
public required Func<string, uint> Attributes;
public required Func<long> Timestamp;
public readonly Dictionary<string, bool> Sparse = new(StringComparer.OrdinalIgnoreCase); // 이 Run 에서 이미 속성을 본 파일
public TouchEnd End = TouchEnd.Completed;
}

/// 뷰를 ms 동안 만지는 사이 늘어난 페이지 읽기 수 (부호 없는 뺄셈 결과) 가운데 디스크에서 읽힌 것으로 볼 바이트: 잡음 허용치를 빼고 뷰 크기를 넘지 않게.
/// 뷰를 ms 동안 만지는 사이 늘어난 페이지 읽기 수 (부호 없는 뺄셈 결과) 가운데 디스크에서 읽힌 것으로 볼 바이트: 잡음 허용치를 빼고 뷰 크기와 그 시간 동안 디스크가 읽을 수 있는 양 (MaxDiskBytesPerSecond) 을 넘지 않게.
public static long DiskBytesOf(uint pagesRead, long viewBytes, double ms)
{
long allowed = NoisePages + (long)(NoisePagesPerSecond * ms / 1000.0);
return Math.Clamp((pagesRead - allowed) * 4096L, 0, Math.Max(0, viewBytes));
long bytes = Math.Clamp((pagesRead - allowed) * 4096L, 0, Math.Max(0, viewBytes));
return Math.Min(bytes, (long)(MaxDiskBytesPerSecond * ms / 1000.0));
}

/// 페이지 읽기 누계가 없을 때: 뷰를 만지는 데 걸린 시간이 바이트에 비례한 기준 (하한 MinSlowMs) 을 넘었는가.
Expand All @@ -90,10 +96,11 @@ public static long DiskBytesOf(uint pagesRead, long viewBytes, double ms)
public static TouchResult Run(IReadOnlyList<ReadOp> ranges, CancellationToken ct, Func<uint?> pageReads) => Run(ranges, ct, pageReads, FileGuards.FileAttributes);

/// attributes: 파일 속성 조회 (예외를 던지면 스파스로 보지 않고 열기를 시도함, 시험은 가짜로 바꿈).
public static TouchResult Run(IReadOnlyList<ReadOp> ranges, CancellationToken ct, Func<uint?> pageReads, Func<string, uint> attributes)
/// timestamp: 뷰를 만지는 데 걸린 시간을 재는 시계 (Stopwatch 틱, 기본은 Stopwatch.GetTimestamp, 시험은 가짜로 바꿈).
public static TouchResult Run(IReadOnlyList<ReadOp> ranges, CancellationToken ct, Func<uint?> pageReads, Func<string, uint> attributes, Func<long>? timestamp = null)
{
long started = Stopwatch.GetTimestamp();
var tally = new Tally { PageReads = pageReads, Attributes = attributes };
var tally = new Tally { PageReads = pageReads, Attributes = attributes, Timestamp = timestamp ?? Stopwatch.GetTimestamp };
var sections = new Dictionary<string, Section?>(StringComparer.OrdinalIgnoreCase);
byte* buffer = (byte*)NativeMemory.Alloc(PieceBytes);
try
Expand Down Expand Up @@ -170,15 +177,15 @@ private static void TouchRange(ReadOp range, Dictionary<string, Section?> sectio
try
{
uint? before = tally.PageReads();
long t0 = Stopwatch.GetTimestamp();
long t0 = tally.Timestamp();
if (!TouchView(view, aligned, pos, viewEnd, buffer, tally, ct))
{
return;
}

if (tally.End == TouchEnd.Completed)
{
NoteDiskReads(tally, before, tally.PageReads(), viewEnd - pos, Stopwatch.GetElapsedTime(t0).TotalMilliseconds);
NoteDiskReads(tally, before, tally.PageReads(), viewEnd - pos, Stopwatch.GetElapsedTime(t0, tally.Timestamp()).TotalMilliseconds);
}
}
finally
Expand Down
63 changes: 54 additions & 9 deletions csharp/tests/AsterCache.Tests/Windows/StandbyTouchTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using AsterCache.Core.Prefetch;
using AsterCache.Windows;
using Xunit;
Expand All @@ -20,7 +21,8 @@ public StandbyTouchTests()
public void Dispose() => _dir.Dispose();

// 범위마다 하나의 뷰: 2 MiB 범위 n 개. 누계는 뷰 전후 두 번 읽히고, 두 번째 읽기에 perView 쪽을 더함 (null 이면 조회 불가).
private TouchResult Run(int ranges, uint? perView)
// viewMs: 뷰마다 가짜 시계가 돌려줄 만지는 시간 (ms) — 시간 상한이 걸린 시간으로 정해지므로 실제 시간에 기대지 않고 정확히 정함.
private TouchResult Run(int ranges, uint? perView, double viewMs = 0)
{
uint value = 1_000_000;
int calls = 0;
Expand All @@ -38,7 +40,15 @@ private TouchResult Run(int ranges, uint? perView)

return value;
};
return StandbyTouch.Run([.. Enumerable.Repeat(new ReadOp(_path, 0, 2 * Mib), ranges)], CancellationToken.None, counter);
return StandbyTouch.Run([.. Enumerable.Repeat(new ReadOp(_path, 0, 2 * Mib), ranges)], CancellationToken.None, counter, _ => Archive, FakeClock(viewMs));
}

// 뷰 하나를 재는 두 번의 호출 (만지기 전, 후) 사이에 ms 가 흐르는 시계 (Stopwatch 틱).
private static Func<long> FakeClock(double viewMs)
{
long ticks = 0;
int calls = 0;
return () => ++calls % 2 == 0 ? ticks += (long)(Stopwatch.Frequency * viewMs / 1000.0) : ticks;
}

[Fact]
Expand All @@ -63,7 +73,7 @@ public void AFewPagesReadByOtherProcessesAreNotCountedAsDiskReads()
[Fact]
public void ViewsReadFromDiskStopOnceMoreThan32MiBAccumulated()
{
TouchResult r = Run(40, 512); // 2 MiB 뷰 전체가 디스크에서 읽힘
TouchResult r = Run(40, 512, 20); // 2 MiB 뷰 전체가 디스크에서 읽힘 (20 ms 동안 디스크는 5 MiB 까지 읽을 수 있음)

Assert.Equal(TouchEnd.NeedsDiskReads, r.End);
Assert.True(r.DiskBytes > StandbyTouch.DiskReadStopBytes);
Expand All @@ -73,13 +83,36 @@ public void ViewsReadFromDiskStopOnceMoreThan32MiBAccumulated()
[Fact]
public void ViewsThatAreOnlyPartlyReadFromDiskStillAddUpAndStop()
{
TouchResult r = Run(400, 256); // 뷰마다 절반만 디스크에서 읽힘: 뷰 단위 판정이면 놓치던 경우
TouchResult r = Run(400, 256, 20); // 뷰마다 절반만 디스크에서 읽힘: 뷰 단위 판정이면 놓치던 경우

Assert.Equal(TouchEnd.NeedsDiskReads, r.End);
Assert.True(r.DiskBytes > StandbyTouch.DiskReadStopBytes);
Assert.True(r.Bytes < 400L * 2 * Mib);
}

[Fact]
public void ViewsTouchedInMemoryNeverAddUpToAStopEvenWhenOtherProcessesReadAHugeNumberOfPagesMeanwhile()
{
_ = _dir.WriteRandom("v.bin", 16 * Mib);
uint value = 1_000_000;
int calls = 0;
Func<uint?> counter = () =>
{
if (++calls % 2 == 0 && calls / 2 % 7 == 0) // 7 번째마다 한 뷰에 다른 프로세스의 몰림 (16 MiB 뷰 전체에 해당하는 쪽 수) 이 겹침
{
value += 4096 * 4;
}

return value;
};

TouchResult r = StandbyTouch.Run([.. Enumerable.Repeat(new ReadOp(_dir.Combine("v.bin"), 0, 16 * Mib), 21)], CancellationToken.None, counter, _ => Archive, FakeClock(5.0)); // 몰림 3 번, 뷰마다 메모리에서 5 ms

Assert.Equal(TouchEnd.Completed, r.End);
Assert.Equal(21L * 16 * Mib, r.Bytes);
Assert.InRange(r.DiskBytes, 4_000_000, 4_050_000); // 몰린 뷰 3 개가 5 ms 동안 디스크가 읽을 수 있는 양 (약 1.28 MiB) 씩만 셈
}

[Fact]
public void WithoutAPageReadCountTheViewTimeRuleIsUsedInstead()
{
Expand Down Expand Up @@ -167,21 +200,33 @@ public void AFilesAttributesAreNotReadAgainAfterTheOpenSectionsAreClearedForTooM
[Theory]
[InlineData(0u, 1 << 20, 0.0, 0L)]
[InlineData(2u, 1 << 20, 0.0, 0L)] // 허용치 이하
[InlineData(3u, 1 << 20, 0.0, 4096L)]
[InlineData(258u, 1 << 20, 0.0, 1L << 20)]
[InlineData(10000u, 1 << 20, 0.0, 1L << 20)] // 뷰 크기를 넘지 않음
[InlineData(3u, 1 << 20, 0.02, 4096L)]
[InlineData(300u, 1 << 20, 4.0, 1L << 20)]
[InlineData(10000u, 1 << 20, 4.0, 1L << 20)] // 뷰 크기를 넘지 않음
[InlineData(2050u, 64 << 20, 1000.0, 0L)] // 1 초 동안의 허용치 2 + 2048 쪽
[InlineData(2051u, 64 << 20, 1000.0, 4096L)]
[InlineData(5u, 0, 0.0, 0L)]
public void DiskBytesAreThePagesReadBeyondTheNoiseAllowanceCappedAtTheView(uint pagesRead, long viewBytes, double ms, long expected) =>
[InlineData(16384u, 64 << 20, 0.0, 0L)] // 시간이 0 이면 디스크가 읽을 수 있는 양도 0
[InlineData(16384u, 64 << 20, 20.0, 5368709L)] // 메모리에서 20 ms: 쪽 수는 뷰 전체여도 20 ms 동안 디스크가 읽을 수 있는 양 (256 MiB/s) 까지만
[InlineData(20000u, 64 << 20, 250.0, 64L << 20)] // 시간 상한이 뷰 크기와 같아지는 경계
[InlineData(16384u, 64 << 20, 500.0, 62906368L)] // 실제로 느린 뷰: 상한 (128 MiB) 이 넉넉해 쪽 수에서 허용치를 뺀 양 그대로
public void DiskBytesAreThePagesReadBeyondTheNoiseAllowanceCappedAtTheViewAndAtWhatTheDiskCouldReadInThatTime(uint pagesRead, long viewBytes, double ms, long expected) =>
Assert.Equal(expected, StandbyTouch.DiskBytesOf(pagesRead, viewBytes, ms));

[Fact]
public void ASlowViewThatReadItsWholeSizeFromDiskIsCountedInFullPastTheStop()
{
long one = StandbyTouch.DiskBytesOf(16384, 64L << 20, 500.0); // 64 MiB 뷰를 0.5 초 (HDD 속도) 에 만짐

Assert.True(one > StandbyTouch.DiskReadStopBytes);
}

[Fact]
public void ACounterThatWrapsAroundIsDifferencedAsUnsigned()
{
uint before = uint.MaxValue - 10;
uint after = unchecked(before + 600); // 넘쳐서 작아짐

Assert.Equal(2L * Mib, StandbyTouch.DiskBytesOf(unchecked(after - before), 2 * Mib, 0));
Assert.Equal(2L * Mib, StandbyTouch.DiskBytesOf(unchecked(after - before), 2 * Mib, 10.0));
}
}
Loading