1432 lines
46 KiB
C#
1432 lines
46 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace SharpEmu.HLE;
|
|
|
|
/// <summary>
|
|
/// Detects guest CPU writes into memory that backs a host GPU image. On PS5
|
|
/// render targets alias unified memory, so games freely mix CPU writes and GPU
|
|
/// draws on the same surface (Chowdren titles memset their fog layers every
|
|
/// frame). Host GPU images are separate storage, so the video backend needs to
|
|
/// know when the guest CPU rewrote a surface to re-upload it. Ranges are
|
|
/// write-protected; the first write faults, the fault handler removes that
|
|
/// image's page watchers and marks it dirty, and the video backend adds the
|
|
/// watchers again after re-uploading. Shared pages stay protected while any
|
|
/// clean image still watches them.
|
|
/// </summary>
|
|
public static unsafe partial class GuestImageWriteTracker
|
|
{
|
|
public readonly record struct ReadSnapshot(
|
|
ulong Address,
|
|
long Generation,
|
|
bool Active);
|
|
|
|
private const int ProtRead = 0x1;
|
|
private const int ProtWrite = 0x2;
|
|
private const int ProtExec = 0x4;
|
|
private const int ClockMonotonicRaw = 4;
|
|
private const ulong TrackingPageSize = 0x1000UL;
|
|
private const int RangeDisarmed = 0;
|
|
private const int RangeArmed = 1;
|
|
private const int RangeInvalidating = 2;
|
|
private const int RangeArming = 3;
|
|
|
|
private sealed class PageState
|
|
{
|
|
public ulong Address;
|
|
public int WriteWatchers;
|
|
public bool Executable;
|
|
}
|
|
|
|
private sealed class TrackedRange
|
|
{
|
|
public ulong Address;
|
|
public ulong ByteCount;
|
|
public ulong Start;
|
|
public ulong End;
|
|
public int Dirty;
|
|
public int Armed;
|
|
/// <summary>
|
|
/// When false the range is watch-only: managed writes still dirty it via
|
|
/// <see cref="NotifyManagedWrite"/>, but pages are never write-protected
|
|
/// so native CPU stores do not fault.
|
|
/// </summary>
|
|
public bool Protect;
|
|
public bool Executable;
|
|
public PageState[] Pages = [];
|
|
public int FirstCpuWriteSeen;
|
|
public int PendingFirstCpuWrite;
|
|
public long WriteGeneration;
|
|
public bool TraceLifetime;
|
|
public long SourceSequence;
|
|
public long FirstCpuWriteTraceSequence;
|
|
public long FirstCpuWriteTimestampNanoseconds;
|
|
public ulong FirstCpuWriteAddress;
|
|
public ulong FirstCpuWritePage;
|
|
public long ProfileArmCount;
|
|
public long ProfileArmBytes;
|
|
public long ProfileFaultCount;
|
|
public long ProfileFaultBytes;
|
|
public bool ManagedWriter;
|
|
public string Source = "unspecified";
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct Timespec
|
|
{
|
|
public long Seconds;
|
|
public long Nanoseconds;
|
|
}
|
|
|
|
private static readonly object _gate = new();
|
|
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
|
private static readonly Dictionary<ulong, TrackedRange> _watchRangesByAddress = new();
|
|
private static readonly Dictionary<ulong, PageState> _pagesByAddress = new();
|
|
|
|
/// <summary>Immutable snapshot read lock-free from the signal handler and
|
|
/// the managed-write pre-visit; rebuilt on every mutation under the gate
|
|
/// (signal handlers must not take managed locks). Carrying the overall
|
|
/// bounds inside the same object keeps the hot-path intersection test
|
|
/// consistent with the array it guards.</summary>
|
|
private sealed class RangeSnapshot
|
|
{
|
|
public static readonly RangeSnapshot Empty = new([]);
|
|
|
|
public readonly TrackedRange[] Ranges;
|
|
public readonly ulong Start;
|
|
public readonly ulong End;
|
|
|
|
public RangeSnapshot(TrackedRange[] ranges)
|
|
{
|
|
Ranges = ranges;
|
|
Start = ulong.MaxValue;
|
|
End = 0;
|
|
foreach (var range in ranges)
|
|
{
|
|
Start = Math.Min(Start, range.Start);
|
|
End = Math.Max(End, range.End);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
|
|
|
|
/// <summary>
|
|
/// Immutable exact-range index for managed writes. Watch-only texture
|
|
/// ranges do not enter the fault snapshot because they must not widen the
|
|
/// native write-fault hot path.
|
|
/// </summary>
|
|
private sealed class WatchRangeSnapshot
|
|
{
|
|
public static readonly WatchRangeSnapshot Empty = new([], []);
|
|
|
|
public readonly TrackedRange[] Ranges;
|
|
public readonly ulong[] PrefixMaximumEnds;
|
|
|
|
public WatchRangeSnapshot(TrackedRange[] ranges, ulong[] prefixMaximumEnds)
|
|
{
|
|
Ranges = ranges;
|
|
PrefixMaximumEnds = prefixMaximumEnds;
|
|
}
|
|
}
|
|
|
|
private static WatchRangeSnapshot _watchRangeSnapshot = WatchRangeSnapshot.Empty;
|
|
|
|
private static bool _enabled;
|
|
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
|
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
|
private static readonly (bool Wildcard, string[] Sources) _lifetimeSourceTraceFilter =
|
|
ParseSourceList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME"));
|
|
private static readonly bool _lifetimeTraceEnabled =
|
|
_lifetimeTraceFilter.Wildcard ||
|
|
_lifetimeTraceFilter.Addresses.Length != 0 ||
|
|
_lifetimeSourceTraceFilter.Wildcard ||
|
|
_lifetimeSourceTraceFilter.Sources.Length != 0;
|
|
private static readonly bool _profileEnabled =
|
|
string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_IMAGE_TRACKER"),
|
|
"1",
|
|
StringComparison.Ordinal) ||
|
|
string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly long _lifetimeTraceEpochNanoseconds =
|
|
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
|
private static long _lifetimeTraceSequence;
|
|
private static long _profileLastReportTimestamp = Stopwatch.GetTimestamp();
|
|
private static long _profileSnapshotCount;
|
|
private static long _profileArmCount;
|
|
private static long _profileArmBytes;
|
|
private static long _profileFaultCount;
|
|
private static long _profileFaultBytes;
|
|
private static long _profileDisarmCount;
|
|
private static long _profileDisarmBytes;
|
|
|
|
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
|
private static extern int Mprotect(nint address, nuint length, int protection);
|
|
|
|
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
|
private static extern int ClockGetTime(int clockId, Timespec* time);
|
|
|
|
public static bool Enabled => _enabled;
|
|
|
|
// Only a backend that keeps CPU snapshots of guest images arms the tracker.
|
|
// The signal handlers can already be installed, so warm the fault path here.
|
|
public static void Configure(bool enabled)
|
|
{
|
|
_enabled = enabled;
|
|
if (enabled)
|
|
{
|
|
WarmUp();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test/diagnostics helper: whether <paramref name="address"/> is tracked
|
|
/// with write protection armed (watch-only ranges report protect=false).
|
|
/// </summary>
|
|
public static bool TryGetProtectionState(
|
|
ulong address,
|
|
out bool protect,
|
|
out bool armed)
|
|
{
|
|
protect = false;
|
|
armed = false;
|
|
lock (_gate)
|
|
{
|
|
if (_rangesByAddress.TryGetValue(address, out var protectedRange))
|
|
{
|
|
protect = true;
|
|
armed = Volatile.Read(ref protectedRange.Armed) == RangeArmed;
|
|
return true;
|
|
}
|
|
|
|
if (!_watchRangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
protect = range.Protect;
|
|
armed = Volatile.Read(ref range.Armed) == RangeArmed;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exercises the fault-handling path once outside signal context so every
|
|
/// branch is JIT-compiled (and, under Rosetta 2, translated) before a real
|
|
/// fault arrives — a cold signal path is silently never entered there.
|
|
/// </summary>
|
|
public static void WarmUp()
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Protection must not cover allocator metadata or other heap allocations.
|
|
var scratch = HostMemory.Alloc(null, (nuint)Environment.SystemPageSize,
|
|
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT, HostMemory.PAGE_READWRITE);
|
|
if (scratch == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
|
// capture path before a real protected-page write reaches it.
|
|
_ = GetMonotonicNanoseconds();
|
|
var address = (ulong)scratch;
|
|
Track(address, 4096);
|
|
_ = TryHandleWriteFault(address);
|
|
_ = ConsumeDirty(address);
|
|
}
|
|
finally
|
|
{
|
|
Untrack((ulong)scratch);
|
|
_ = HostMemory.Free(scratch, 0, HostMemory.MEM_RELEASE);
|
|
}
|
|
}
|
|
|
|
/// <summary>Enable write protection when <paramref name="protect"/> is true.
|
|
/// Otherwise, track only writes reported through <see cref="NotifyManagedWrite"/>.</summary>
|
|
public static void Track(
|
|
ulong address,
|
|
ulong byteCount,
|
|
long sourceSequence = 0,
|
|
string source = "unspecified",
|
|
bool protect = true)
|
|
{
|
|
if (address == 0 || byteCount == 0 || (protect && !_enabled))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!protect)
|
|
{
|
|
TrackWatchOnly(address, byteCount, sourceSequence, source);
|
|
return;
|
|
}
|
|
|
|
var (start, length) = PageAlign(address, byteCount);
|
|
lock (_gate)
|
|
{
|
|
_rangesByAddress.TryGetValue(address, out var range);
|
|
if (range is not null &&
|
|
(range.Start != start ||
|
|
range.End != start + length ||
|
|
range.ByteCount != byteCount))
|
|
{
|
|
// Never resize an object that is still reachable from the
|
|
// signal handler's lock-free snapshot. Retire it and publish
|
|
// a fresh immutable range, carrying the write generation so
|
|
// resizes do not hide guest CPU rewrites from cache owners.
|
|
var writeGeneration = Volatile.Read(ref range.WriteGeneration);
|
|
var keepProtect = range.Protect || protect;
|
|
DisarmLocked(range, "replace-range");
|
|
_rangesByAddress.Remove(address);
|
|
range = new TrackedRange
|
|
{
|
|
Address = address,
|
|
ByteCount = byteCount,
|
|
Start = start,
|
|
End = start + length,
|
|
Protect = keepProtect,
|
|
Executable = range.Executable || IsExecutableMapping(address),
|
|
WriteGeneration = writeGeneration,
|
|
};
|
|
range.Pages = GetTrackedPagesLocked(
|
|
range.Start,
|
|
range.End,
|
|
range.Executable);
|
|
_rangesByAddress[address] = range;
|
|
RebuildSnapshotLocked();
|
|
}
|
|
|
|
if (range is null)
|
|
{
|
|
range = new TrackedRange
|
|
{
|
|
Address = address,
|
|
ByteCount = byteCount,
|
|
Start = start,
|
|
End = start + length,
|
|
Protect = protect,
|
|
Executable = IsExecutableMapping(address),
|
|
TraceLifetime =
|
|
ShouldTraceRange(start, start + length) || ShouldTraceSource(source),
|
|
SourceSequence = sourceSequence,
|
|
Source = source,
|
|
};
|
|
range.Pages = GetTrackedPagesLocked(
|
|
range.Start,
|
|
range.End,
|
|
range.Executable);
|
|
_rangesByAddress[address] = range;
|
|
RebuildSnapshotLocked();
|
|
}
|
|
else
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
// Protect is sticky: a later watch-only Track (texture cache)
|
|
// must not disarm an RT that already needs page faults.
|
|
if (protect && !range.Protect)
|
|
{
|
|
range.Protect = true;
|
|
}
|
|
}
|
|
|
|
range.SourceSequence = sourceSequence;
|
|
range.Source = source;
|
|
range.TraceLifetime =
|
|
ShouldTraceRange(range.Start, range.End) || ShouldTraceSource(source);
|
|
if (range.Protect)
|
|
{
|
|
ArmLocked(range, "arm");
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Untrack(ulong address)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
DisarmLocked(range, "untrack");
|
|
_rangesByAddress.Remove(address);
|
|
RebuildSnapshotLocked();
|
|
}
|
|
|
|
if (_watchRangesByAddress.Remove(address))
|
|
{
|
|
RebuildWatchSnapshotLocked();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes only native write protection for a guest image. A sampled
|
|
/// texture observer at the same address remains active.
|
|
/// </summary>
|
|
public static void UntrackProtected(ulong address)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
DisarmLocked(range, "untrack");
|
|
_rangesByAddress.Remove(address);
|
|
RebuildSnapshotLocked();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes only the managed-write observer for a sampled texture. A render
|
|
/// target at the same address keeps its native write protection.
|
|
/// </summary>
|
|
public static void UntrackWatchOnly(ulong address)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_watchRangesByAddress.Remove(address))
|
|
{
|
|
RebuildWatchSnapshotLocked();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the guest CPU wrote the range since the last call,
|
|
/// clearing the flag. The caller re-arms via <see cref="Rearm"/> after it
|
|
/// finished reading the guest bytes.
|
|
/// </summary>
|
|
public static bool ConsumeDirty(ulong address)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
var dirty = false;
|
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
dirty |= Interlocked.Exchange(ref range.Dirty, 0) != 0;
|
|
}
|
|
|
|
if (_watchRangesByAddress.TryGetValue(address, out var watchRange))
|
|
{
|
|
dirty |= Interlocked.Exchange(ref watchRange.Dirty, 0) != 0;
|
|
}
|
|
|
|
return dirty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Non-consuming variant of <see cref="ConsumeDirty"/>: reports whether
|
|
/// the range has been written since it was last re-armed, leaving the
|
|
/// flag for the owner that evicts and re-uploads.
|
|
/// </summary>
|
|
public static bool PeekDirty(ulong address)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
if (Volatile.Read(ref range.Dirty) != 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return _watchRangesByAddress.TryGetValue(address, out var watchRange) &&
|
|
Volatile.Read(ref watchRange.Dirty) != 0;
|
|
}
|
|
}
|
|
|
|
public static void Rearm(ulong address)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_gate)
|
|
{
|
|
if (_rangesByAddress.TryGetValue(address, out var range) &&
|
|
range.Protect)
|
|
{
|
|
ArmLocked(range, "rearm");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the monotonic first-write generation for a tracked allocation.
|
|
/// Unlike the consuming dirty flag, this remains changed after another
|
|
/// cache owner consumes and re-arms the range.
|
|
/// </summary>
|
|
public static bool TryGetWriteGeneration(ulong address, out long generation)
|
|
{
|
|
generation = 0;
|
|
lock (_gate)
|
|
{
|
|
var found = false;
|
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
generation = Volatile.Read(ref range.WriteGeneration);
|
|
found = true;
|
|
}
|
|
|
|
if (_watchRangesByAddress.TryGetValue(address, out var watchRange))
|
|
{
|
|
generation = unchecked(generation + Volatile.Read(ref watchRange.WriteGeneration));
|
|
found = true;
|
|
}
|
|
|
|
return found;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
|
/// POSIX signal bridge, but a managed Buffer.MemoryCopy into a protected
|
|
/// page is surfaced by the runtime as a fatal AccessViolation instead of
|
|
/// a resumable guest fault. Visit every page in the write span up front so
|
|
/// all overlapping texture owners are dirtied and made writable.
|
|
/// </summary>
|
|
public static void NotifyManagedWrite(ulong address, ulong byteCount)
|
|
{
|
|
if (address == 0 || byteCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var end = address > ulong.MaxValue - byteCount
|
|
? ulong.MaxValue
|
|
: address + byteCount;
|
|
|
|
// Fast rejection for the hot path: this runs on every managed guest
|
|
// write, and almost none of them touch tracked texture pages. The
|
|
// bounds live inside the snapshot so they are always consistent with
|
|
// the ranges the per-page visit below would consult.
|
|
MarkManagedWatchRanges(address, end);
|
|
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshot = Volatile.Read(ref _rangeSnapshot);
|
|
if (snapshot.Ranges.Length == 0 || end <= snapshot.Start || address >= snapshot.End)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var candidate = address;
|
|
while (candidate < end)
|
|
{
|
|
_ = TryHandleWriteFault(candidate);
|
|
var nextPage = (candidate & ~0xFFFUL) + 0x1000UL;
|
|
if (nextPage <= candidate)
|
|
{
|
|
break;
|
|
}
|
|
candidate = nextPage;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flushes scalar first-write records captured by the POSIX signal handler.
|
|
/// Call only from ordinary managed execution, never from signal context.
|
|
/// </summary>
|
|
public static void FlushPendingDiagnostics()
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_lifetimeTraceEnabled)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
foreach (var range in _rangesByAddress.Values)
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
}
|
|
}
|
|
}
|
|
|
|
ReportProfileIfDue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Signal-handler entry: if the fault address lies in a tracked, armed
|
|
/// range, restore write access, mark the range dirty, and return true so
|
|
/// the faulting write can be retried. Must not allocate or lock.
|
|
/// </summary>
|
|
public static bool TryHandleWriteFault(ulong faultAddress)
|
|
{
|
|
if (!_enabled || faultAddress == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var faultPage = faultAddress & ~(TrackingPageSize - 1);
|
|
var ranges = Volatile.Read(ref _rangeSnapshot).Ranges;
|
|
var faultPageEnd = faultPage + TrackingPageSize;
|
|
var handled = false;
|
|
long writableBytes = 0;
|
|
|
|
// Invalidate every image whose exact guest byte range contains the
|
|
// write. Removing that image's watchers from all pages lets a
|
|
// sequential CPU update continue without one exception per page.
|
|
for (var index = 0; index < ranges.Length; index++)
|
|
{
|
|
var range = ranges[index];
|
|
var exactEnd = range.Address > ulong.MaxValue - range.ByteCount
|
|
? ulong.MaxValue
|
|
: range.Address + range.ByteCount;
|
|
if (faultAddress < range.Address || faultAddress >= exactEnd)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var previousState = Interlocked.CompareExchange(
|
|
ref range.Armed,
|
|
RangeInvalidating,
|
|
RangeArmed);
|
|
if (previousState is RangeInvalidating or RangeArming)
|
|
{
|
|
handled = true;
|
|
continue;
|
|
}
|
|
if (previousState != RangeArmed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!RemovePageWatchers(range, out var changedBytes))
|
|
{
|
|
Volatile.Write(ref range.Armed, RangeDisarmed);
|
|
return false;
|
|
}
|
|
|
|
writableBytes += changedBytes;
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref range.ProfileFaultCount);
|
|
Interlocked.Add(ref range.ProfileFaultBytes, changedBytes);
|
|
}
|
|
MarkFaultedRange(range, faultAddress);
|
|
Volatile.Write(ref range.Armed, RangeDisarmed);
|
|
handled = true;
|
|
}
|
|
|
|
// Page protection is coarser than an image's exact byte range. A
|
|
// boundary page can still have a watcher from an image whose bytes do
|
|
// not contain this address. Remove that owner too so retrying the same
|
|
// store cannot fault forever. This is limited to the one shared page;
|
|
// unrelated overlap chains are not invalidated.
|
|
if (IsPageStillWatched(ranges, faultPage))
|
|
{
|
|
for (var index = 0; index < ranges.Length; index++)
|
|
{
|
|
var range = ranges[index];
|
|
if (range.Start >= faultPageEnd || range.End <= faultPage)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
var previousState = Interlocked.CompareExchange(
|
|
ref range.Armed,
|
|
RangeInvalidating,
|
|
RangeArmed);
|
|
if (previousState is RangeInvalidating or RangeArming)
|
|
{
|
|
handled = true;
|
|
continue;
|
|
}
|
|
if (previousState != RangeArmed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!RemovePageWatchers(range, out var changedBytes))
|
|
{
|
|
Volatile.Write(ref range.Armed, RangeDisarmed);
|
|
return false;
|
|
}
|
|
|
|
writableBytes += changedBytes;
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref range.ProfileFaultCount);
|
|
Interlocked.Add(ref range.ProfileFaultBytes, changedBytes);
|
|
}
|
|
MarkFaultedRange(range, faultAddress);
|
|
Volatile.Write(ref range.Armed, RangeDisarmed);
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
if (!handled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref _profileFaultCount);
|
|
Interlocked.Add(ref _profileFaultBytes, writableBytes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void ArmLocked(TrackedRange range, string operation)
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
if (Interlocked.CompareExchange(
|
|
ref range.Armed,
|
|
RangeArming,
|
|
RangeDisarmed) != RangeDisarmed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A new publication/rearm starts a new first-write lifetime.
|
|
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
|
var failed = !AddPageWatchers(range, out var protectedBytes);
|
|
if (failed)
|
|
{
|
|
Volatile.Write(ref range.Armed, RangeDisarmed);
|
|
}
|
|
else
|
|
{
|
|
Volatile.Write(ref range.Armed, RangeArmed);
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref _profileArmCount);
|
|
Interlocked.Add(ref _profileArmBytes, protectedBytes);
|
|
Interlocked.Increment(ref range.ProfileArmCount);
|
|
Interlocked.Add(ref range.ProfileArmBytes, protectedBytes);
|
|
}
|
|
}
|
|
|
|
if (range.TraceLifetime)
|
|
{
|
|
TraceLifetime(
|
|
range,
|
|
failed ? $"{operation}-failed-errno-{Marshal.GetLastPInvokeError()}" : operation);
|
|
}
|
|
}
|
|
|
|
private static void DisarmLocked(TrackedRange range, string operation)
|
|
{
|
|
FlushPendingFirstCpuWrite(range);
|
|
var wasArmed = Interlocked.CompareExchange(
|
|
ref range.Armed,
|
|
RangeDisarmed,
|
|
RangeArmed) == RangeArmed;
|
|
if (wasArmed && RemovePageWatchers(range, out var writableBytes))
|
|
{
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref _profileDisarmCount);
|
|
Interlocked.Add(ref _profileDisarmBytes, writableBytes);
|
|
}
|
|
}
|
|
|
|
if (range.TraceLifetime)
|
|
{
|
|
TraceLifetime(
|
|
range,
|
|
wasArmed ? operation : $"{operation}-already-disarmed");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a range written only through managed HLE memory helpers.
|
|
/// Those helpers notify the tracker before each write, so page protection
|
|
/// would duplicate the same observation and add avoidable fault traffic.
|
|
/// </summary>
|
|
public static void TrackManagedWriter(
|
|
ulong address,
|
|
ulong byteCount,
|
|
long sourceSequence = 0,
|
|
string source = "managed-writer")
|
|
{
|
|
if (address == 0 || byteCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TrackWatchOnly(
|
|
address,
|
|
byteCount,
|
|
sourceSequence,
|
|
source,
|
|
managedWriter: true);
|
|
}
|
|
|
|
private static PageState[] GetTrackedPagesLocked(
|
|
ulong start,
|
|
ulong end,
|
|
bool executable)
|
|
{
|
|
var pageCount = checked((int)((end - start) / TrackingPageSize));
|
|
var pages = new PageState[pageCount];
|
|
for (var index = 0; index < pageCount; index++)
|
|
{
|
|
var pageAddress = start + (ulong)index * TrackingPageSize;
|
|
if (!_pagesByAddress.TryGetValue(pageAddress, out var page))
|
|
{
|
|
page = new PageState
|
|
{
|
|
Address = pageAddress,
|
|
Executable = executable,
|
|
};
|
|
_pagesByAddress[pageAddress] = page;
|
|
}
|
|
else if (Volatile.Read(ref page.WriteWatchers) == 0)
|
|
{
|
|
// A guest mapping can be recycled with different execute
|
|
// permissions after its previous image owner retired.
|
|
page.Executable = executable;
|
|
}
|
|
else
|
|
{
|
|
page.Executable |= executable;
|
|
}
|
|
|
|
pages[index] = page;
|
|
}
|
|
|
|
return pages;
|
|
}
|
|
|
|
private static bool AddPageWatchers(TrackedRange range, out long protectedBytes)
|
|
{
|
|
protectedBytes = 0;
|
|
var runStart = 0UL;
|
|
var runEnd = 0UL;
|
|
var runExecutable = false;
|
|
var processedPages = 0;
|
|
|
|
for (var index = 0; index < range.Pages.Length; index++)
|
|
{
|
|
var page = range.Pages[index];
|
|
var watcherCount = Interlocked.Increment(ref page.WriteWatchers);
|
|
processedPages++;
|
|
if (watcherCount != 1)
|
|
{
|
|
if (!FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: false,
|
|
ref protectedBytes))
|
|
{
|
|
_ = RemovePageWatchers(range, processedPages, out _);
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (runEnd == page.Address && runExecutable == page.Executable)
|
|
{
|
|
runEnd += TrackingPageSize;
|
|
continue;
|
|
}
|
|
|
|
if (!FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: false,
|
|
ref protectedBytes))
|
|
{
|
|
_ = RemovePageWatchers(range, processedPages, out _);
|
|
return false;
|
|
}
|
|
|
|
runStart = page.Address;
|
|
runEnd = page.Address + TrackingPageSize;
|
|
runExecutable = page.Executable;
|
|
}
|
|
|
|
if (FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: false,
|
|
ref protectedBytes))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_ = RemovePageWatchers(range, processedPages, out _);
|
|
return false;
|
|
}
|
|
|
|
private static bool RemovePageWatchers(TrackedRange range, out long writableBytes) =>
|
|
RemovePageWatchers(range, range.Pages.Length, out writableBytes);
|
|
|
|
private static bool RemovePageWatchers(
|
|
TrackedRange range,
|
|
int pageCount,
|
|
out long writableBytes)
|
|
{
|
|
writableBytes = 0;
|
|
var runStart = 0UL;
|
|
var runEnd = 0UL;
|
|
var runExecutable = false;
|
|
|
|
for (var index = 0; index < pageCount; index++)
|
|
{
|
|
var page = range.Pages[index];
|
|
var watcherCount = Interlocked.Decrement(ref page.WriteWatchers);
|
|
if (watcherCount < 0)
|
|
{
|
|
Interlocked.Increment(ref page.WriteWatchers);
|
|
return false;
|
|
}
|
|
|
|
if (watcherCount != 0)
|
|
{
|
|
if (!FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: true,
|
|
ref writableBytes))
|
|
{
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (runEnd == page.Address && runExecutable == page.Executable)
|
|
{
|
|
runEnd += TrackingPageSize;
|
|
continue;
|
|
}
|
|
|
|
if (!FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: true,
|
|
ref writableBytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
runStart = page.Address;
|
|
runEnd = page.Address + TrackingPageSize;
|
|
runExecutable = page.Executable;
|
|
}
|
|
|
|
return FlushProtectionRun(
|
|
ref runStart,
|
|
ref runEnd,
|
|
runExecutable,
|
|
writable: true,
|
|
ref writableBytes);
|
|
}
|
|
|
|
private static bool FlushProtectionRun(
|
|
ref ulong runStart,
|
|
ref ulong runEnd,
|
|
bool executable,
|
|
bool writable,
|
|
ref long changedBytes)
|
|
{
|
|
if (runEnd <= runStart)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var byteCount = runEnd - runStart;
|
|
if (!TrySetProtection(runStart, byteCount, writable, executable))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
changedBytes += (long)byteCount;
|
|
runStart = 0;
|
|
runEnd = 0;
|
|
return true;
|
|
}
|
|
|
|
private static bool IsPageStillWatched(TrackedRange[] ranges, ulong pageAddress)
|
|
{
|
|
var pageEnd = pageAddress + TrackingPageSize;
|
|
for (var index = 0; index < ranges.Length; index++)
|
|
{
|
|
var range = ranges[index];
|
|
if (Volatile.Read(ref range.Armed) != RangeDisarmed &&
|
|
range.Start < pageEnd && range.End > pageAddress)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void MarkFaultedRange(TrackedRange range, ulong faultAddress)
|
|
{
|
|
Interlocked.Exchange(ref range.Dirty, 1);
|
|
Interlocked.Increment(ref range.WriteGeneration);
|
|
if (!range.TraceLifetime ||
|
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Signal context: capture preallocated scalar fields only. Formatting
|
|
// and I/O are deferred to a locked safe path.
|
|
range.FirstCpuWriteTraceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
|
|
range.FirstCpuWriteTimestampNanoseconds = GetMonotonicNanoseconds();
|
|
range.FirstCpuWriteAddress = faultAddress;
|
|
range.FirstCpuWritePage = faultAddress & ~(TrackingPageSize - 1);
|
|
Volatile.Write(ref range.PendingFirstCpuWrite, 1);
|
|
Volatile.Write(ref range.FirstCpuWriteSeen, 2);
|
|
}
|
|
|
|
private static void RebuildSnapshotLocked()
|
|
{
|
|
Volatile.Write(ref _rangeSnapshot, new RangeSnapshot(_rangesByAddress.Values.ToArray()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Arms write observation before a caller copies guest image memory and
|
|
/// returns the generation that must still be current when the copy ends.
|
|
/// </summary>
|
|
public static ReadSnapshot BeginReadSnapshot(
|
|
ulong address,
|
|
ulong byteCount,
|
|
long sourceSequence = 0,
|
|
string source = "texture-snapshot")
|
|
{
|
|
if (!_enabled || address == 0 || byteCount == 0)
|
|
{
|
|
return new ReadSnapshot(address, -1, false);
|
|
}
|
|
|
|
if (_profileEnabled)
|
|
{
|
|
Interlocked.Increment(ref _profileSnapshotCount);
|
|
}
|
|
|
|
if (!HasManagedWriterCoverage(address, byteCount))
|
|
{
|
|
Track(address, byteCount, sourceSequence, source, protect: true);
|
|
}
|
|
return TryGetWriteGeneration(address, out var generation)
|
|
? new ReadSnapshot(address, generation, true)
|
|
: new ReadSnapshot(address, -1, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true only when no observed guest write overlapped the snapshot.
|
|
/// </summary>
|
|
public static bool IsReadSnapshotStable(in ReadSnapshot snapshot) =>
|
|
!snapshot.Active ||
|
|
(TryGetWriteGeneration(snapshot.Address, out var generation) &&
|
|
generation == snapshot.Generation);
|
|
|
|
private static void TrackWatchOnly(
|
|
ulong address,
|
|
ulong byteCount,
|
|
long sourceSequence,
|
|
string source,
|
|
bool managedWriter = false)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_watchRangesByAddress.TryGetValue(address, out var range))
|
|
{
|
|
// Several views can share one allocation. Keep the largest
|
|
// observed extent so a narrower view cannot hide later writes.
|
|
if (byteCount <= range.ByteCount)
|
|
{
|
|
range.SourceSequence = sourceSequence;
|
|
range.Source = source;
|
|
range.ManagedWriter |= managedWriter;
|
|
return;
|
|
}
|
|
|
|
var generation = Volatile.Read(ref range.WriteGeneration);
|
|
var dirty = Volatile.Read(ref range.Dirty);
|
|
range = new TrackedRange
|
|
{
|
|
Address = address,
|
|
ByteCount = byteCount,
|
|
Start = address,
|
|
End = SaturatingEnd(address, byteCount),
|
|
Protect = false,
|
|
Dirty = dirty,
|
|
WriteGeneration = generation,
|
|
ManagedWriter = range.ManagedWriter || managedWriter,
|
|
SourceSequence = sourceSequence,
|
|
Source = source,
|
|
};
|
|
_watchRangesByAddress[address] = range;
|
|
RebuildWatchSnapshotLocked();
|
|
return;
|
|
}
|
|
|
|
_watchRangesByAddress[address] = new TrackedRange
|
|
{
|
|
Address = address,
|
|
ByteCount = byteCount,
|
|
Start = address,
|
|
End = SaturatingEnd(address, byteCount),
|
|
Protect = false,
|
|
ManagedWriter = managedWriter,
|
|
SourceSequence = sourceSequence,
|
|
Source = source,
|
|
};
|
|
RebuildWatchSnapshotLocked();
|
|
}
|
|
}
|
|
|
|
private static void RebuildWatchSnapshotLocked()
|
|
{
|
|
var ranges = _watchRangesByAddress.Values
|
|
.OrderBy(static range => range.Start)
|
|
.ToArray();
|
|
var prefixMaximumEnds = new ulong[ranges.Length];
|
|
var maximumEnd = 0UL;
|
|
for (var index = 0; index < ranges.Length; index++)
|
|
{
|
|
maximumEnd = Math.Max(maximumEnd, ranges[index].End);
|
|
prefixMaximumEnds[index] = maximumEnd;
|
|
}
|
|
|
|
Volatile.Write(
|
|
ref _watchRangeSnapshot,
|
|
new WatchRangeSnapshot(ranges, prefixMaximumEnds));
|
|
}
|
|
|
|
private static void MarkManagedWatchRanges(ulong address, ulong end)
|
|
{
|
|
var snapshot = Volatile.Read(ref _watchRangeSnapshot);
|
|
var ranges = snapshot.Ranges;
|
|
if (ranges.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var low = 0;
|
|
var high = ranges.Length;
|
|
while (low < high)
|
|
{
|
|
var middle = low + ((high - low) / 2);
|
|
if (ranges[middle].Start < end)
|
|
{
|
|
low = middle + 1;
|
|
}
|
|
else
|
|
{
|
|
high = middle;
|
|
}
|
|
}
|
|
|
|
for (var index = low - 1;
|
|
index >= 0 && snapshot.PrefixMaximumEnds[index] > address;
|
|
index--)
|
|
{
|
|
var range = ranges[index];
|
|
if (range.End <= address)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Interlocked.Exchange(ref range.Dirty, 1) == 0)
|
|
{
|
|
Interlocked.Increment(ref range.WriteGeneration);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static ulong SaturatingEnd(ulong address, ulong byteCount) =>
|
|
address > ulong.MaxValue - byteCount ? ulong.MaxValue : address + byteCount;
|
|
|
|
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
|
{
|
|
const ulong pageMask = 0xFFFUL;
|
|
var start = address & ~pageMask;
|
|
var end = (address + byteCount + pageMask) & ~pageMask;
|
|
return (start, end - start);
|
|
}
|
|
|
|
private static bool ShouldTraceRange(ulong start, ulong end)
|
|
{
|
|
if (_lifetimeTraceFilter.Wildcard)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var addresses = _lifetimeTraceFilter.Addresses;
|
|
for (var index = 0; index < addresses.Length; index++)
|
|
{
|
|
if (addresses[index] >= start && addresses[index] < end)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static (bool Wildcard, ulong[] Addresses) ParseAddressList(string? addresses)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(addresses))
|
|
{
|
|
return (false, []);
|
|
}
|
|
|
|
var parsedAddresses = new List<ulong>();
|
|
foreach (var token in addresses.Split(
|
|
[',', ';', ' ', '\t'],
|
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
{
|
|
if (token == "*")
|
|
{
|
|
return (true, []);
|
|
}
|
|
|
|
var span = token.AsSpan();
|
|
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
span = span[2..];
|
|
}
|
|
|
|
if (ulong.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
|
{
|
|
parsedAddresses.Add(parsed);
|
|
}
|
|
}
|
|
|
|
return (false, parsedAddresses.ToArray());
|
|
}
|
|
|
|
private static bool ShouldTraceSource(string source)
|
|
{
|
|
if (_lifetimeSourceTraceFilter.Wildcard)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return Array.IndexOf(_lifetimeSourceTraceFilter.Sources, source) >= 0;
|
|
}
|
|
|
|
private static (bool Wildcard, string[] Sources) ParseSourceList(string? sources)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sources))
|
|
{
|
|
return (false, []);
|
|
}
|
|
|
|
var parsedSources = new List<string>();
|
|
foreach (var token in sources.Split(
|
|
[',', ';'],
|
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
{
|
|
if (token == "*")
|
|
{
|
|
return (true, []);
|
|
}
|
|
|
|
parsedSources.Add(token);
|
|
}
|
|
|
|
return (false, parsedSources.ToArray());
|
|
}
|
|
|
|
private static void FlushPendingFirstCpuWrite(TrackedRange range)
|
|
{
|
|
var spin = new SpinWait();
|
|
while (Volatile.Read(ref range.FirstCpuWriteSeen) == 1)
|
|
{
|
|
spin.SpinOnce();
|
|
}
|
|
|
|
if (!range.TraceLifetime || Interlocked.Exchange(ref range.PendingFirstCpuWrite, 0) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TraceLifetime(
|
|
range,
|
|
"first-cpu-write-disarm",
|
|
range.FirstCpuWriteAddress,
|
|
range.FirstCpuWritePage,
|
|
range.FirstCpuWriteTraceSequence,
|
|
range.FirstCpuWriteTimestampNanoseconds);
|
|
}
|
|
|
|
private static void TraceLifetime(
|
|
TrackedRange range,
|
|
string operation,
|
|
ulong faultAddress = 0,
|
|
ulong faultPage = 0,
|
|
long traceSequence = 0,
|
|
long timestampNanoseconds = 0)
|
|
{
|
|
if (traceSequence == 0)
|
|
{
|
|
traceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
|
|
}
|
|
|
|
if (timestampNanoseconds == 0)
|
|
{
|
|
timestampNanoseconds = GetMonotonicNanoseconds();
|
|
}
|
|
|
|
var elapsedMilliseconds =
|
|
(timestampNanoseconds - _lifetimeTraceEpochNanoseconds) / 1_000_000.0;
|
|
Console.Error.WriteLine(
|
|
$"[WT][LIFETIME] seq={traceSequence} t_ms={elapsedMilliseconds:F3} " +
|
|
$"event={operation} source_seq={range.SourceSequence} source='{range.Source}' " +
|
|
$"requested=0x{range.Address:X16}+0x{range.ByteCount:X} " +
|
|
$"range=0x{range.Start:X16}..0x{range.End:X16} " +
|
|
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
|
|
}
|
|
|
|
private static bool TrySetProtection(
|
|
ulong start,
|
|
ulong length,
|
|
bool writable,
|
|
bool executable)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var protection = ProtRead |
|
|
(writable ? ProtWrite : 0) |
|
|
(executable ? ProtExec : 0);
|
|
return Mprotect(
|
|
(nint)start,
|
|
(nuint)length,
|
|
protection) == 0;
|
|
}
|
|
|
|
private static bool IsExecutableMapping(ulong address)
|
|
{
|
|
if (address == 0 ||
|
|
HostMemory.Query((void*)address, out var info) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var protection = info.Protect & 0xFFu;
|
|
return protection is
|
|
HostMemory.PAGE_EXECUTE or
|
|
HostMemory.PAGE_EXECUTE_READ or
|
|
HostMemory.PAGE_EXECUTE_READWRITE;
|
|
}
|
|
|
|
private static long GetMonotonicNanoseconds()
|
|
{
|
|
Timespec time;
|
|
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
|
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
|
: 0;
|
|
}
|
|
|
|
private static void ReportProfileIfDue()
|
|
{
|
|
if (!_profileEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var now = Stopwatch.GetTimestamp();
|
|
var previous = Volatile.Read(ref _profileLastReportTimestamp);
|
|
if (now - previous < Stopwatch.Frequency * 5 ||
|
|
Interlocked.CompareExchange(ref _profileLastReportTimestamp, now, previous) != previous)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshots = Interlocked.Exchange(ref _profileSnapshotCount, 0);
|
|
var arms = Interlocked.Exchange(ref _profileArmCount, 0);
|
|
var armBytes = Interlocked.Exchange(ref _profileArmBytes, 0);
|
|
var faults = Interlocked.Exchange(ref _profileFaultCount, 0);
|
|
var faultBytes = Interlocked.Exchange(ref _profileFaultBytes, 0);
|
|
var disarms = Interlocked.Exchange(ref _profileDisarmCount, 0);
|
|
var disarmBytes = Interlocked.Exchange(ref _profileDisarmBytes, 0);
|
|
int protectedRanges;
|
|
List<(ulong Address, ulong ByteCount, string Source, long Arms, long ArmBytes, long Faults, long FaultBytes)> hotRanges;
|
|
lock (_gate)
|
|
{
|
|
protectedRanges = _rangesByAddress.Count;
|
|
hotRanges = new(protectedRanges);
|
|
foreach (var range in _rangesByAddress.Values)
|
|
{
|
|
var rangeArms = Interlocked.Exchange(ref range.ProfileArmCount, 0);
|
|
var rangeArmBytes = Interlocked.Exchange(ref range.ProfileArmBytes, 0);
|
|
var rangeFaults = Interlocked.Exchange(ref range.ProfileFaultCount, 0);
|
|
var rangeFaultBytes = Interlocked.Exchange(ref range.ProfileFaultBytes, 0);
|
|
if (rangeArms == 0 && rangeFaults == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
hotRanges.Add((
|
|
range.Address,
|
|
range.ByteCount,
|
|
range.Source,
|
|
rangeArms,
|
|
rangeArmBytes,
|
|
rangeFaults,
|
|
rangeFaultBytes));
|
|
}
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[PERF][GUEST_IMAGE_TRACKER] snapshots={snapshots} " +
|
|
$"arms={arms}/{armBytes}B faults={faults}/{faultBytes}B " +
|
|
$"disarms={disarms}/{disarmBytes}B ranges={protectedRanges}");
|
|
|
|
foreach (var range in hotRanges
|
|
.OrderByDescending(static range => Math.Max(range.ArmBytes, range.FaultBytes))
|
|
.ThenByDescending(static range => range.Faults)
|
|
.Take(5))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[PERF][GUEST_IMAGE_TRACKER_RANGE] addr=0x{range.Address:X16} " +
|
|
$"size={range.ByteCount}B source='{range.Source}' " +
|
|
$"arms={range.Arms}/{range.ArmBytes}B " +
|
|
$"faults={range.Faults}/{range.FaultBytes}B");
|
|
}
|
|
}
|
|
|
|
private static bool HasManagedWriterCoverage(ulong address, ulong byteCount)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _watchRangesByAddress.TryGetValue(address, out var range) &&
|
|
range.ManagedWriter &&
|
|
range.ByteCount >= byteCount;
|
|
}
|
|
}
|
|
}
|