PtcProfiler.cs
1 using ARMeilleure.State; 2 using Ryujinx.Common; 3 using Ryujinx.Common.Logging; 4 using Ryujinx.Common.Memory; 5 using System; 6 using System.Buffers.Binary; 7 using System.Collections.Concurrent; 8 using System.Collections.Generic; 9 using System.Diagnostics; 10 using System.IO; 11 using System.IO.Compression; 12 using System.Linq; 13 using System.Runtime.CompilerServices; 14 using System.Runtime.InteropServices; 15 using System.Threading; 16 using System.Timers; 17 using static ARMeilleure.Translation.PTC.PtcFormatter; 18 using Timer = System.Timers.Timer; 19 20 namespace ARMeilleure.Translation.PTC 21 { 22 class PtcProfiler 23 { 24 private const string OuterHeaderMagicString = "Pohd\0\0\0\0"; 25 26 private const uint InternalVersion = 5518; //! Not to be incremented manually for each change to the ARMeilleure project. 27 28 private static readonly uint[] _migrateInternalVersions = { 29 1866, 30 }; 31 32 private const int SaveInterval = 30; // Seconds. 33 34 private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest; 35 36 private readonly Ptc _ptc; 37 38 private readonly Timer _timer; 39 40 private readonly ulong _outerHeaderMagic; 41 42 private readonly ManualResetEvent _waitEvent; 43 44 private readonly object _lock; 45 46 private bool _disposed; 47 48 private Hash128 _lastHash; 49 50 public Dictionary<ulong, FuncProfile> ProfiledFuncs { get; private set; } 51 52 public bool Enabled { get; private set; } 53 54 public ulong StaticCodeStart { get; set; } 55 public ulong StaticCodeSize { get; set; } 56 57 public PtcProfiler(Ptc ptc) 58 { 59 _ptc = ptc; 60 61 _timer = new Timer(SaveInterval * 1000d); 62 _timer.Elapsed += PreSave; 63 64 _outerHeaderMagic = BinaryPrimitives.ReadUInt64LittleEndian(EncodingCache.UTF8NoBOM.GetBytes(OuterHeaderMagicString).AsSpan()); 65 66 _waitEvent = new ManualResetEvent(true); 67 68 _lock = new object(); 69 70 _disposed = false; 71 72 ProfiledFuncs = new Dictionary<ulong, FuncProfile>(); 73 74 Enabled = false; 75 } 76 77 public void AddEntry(ulong address, ExecutionMode mode, bool highCq) 78 { 79 if (IsAddressInStaticCodeRange(address)) 80 { 81 Debug.Assert(!highCq); 82 83 lock (_lock) 84 { 85 ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false)); 86 } 87 } 88 } 89 90 public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq) 91 { 92 if (IsAddressInStaticCodeRange(address)) 93 { 94 Debug.Assert(highCq); 95 96 lock (_lock) 97 { 98 Debug.Assert(ProfiledFuncs.ContainsKey(address)); 99 100 ProfiledFuncs[address] = new FuncProfile(mode, highCq: true); 101 } 102 } 103 } 104 105 public bool IsAddressInStaticCodeRange(ulong address) 106 { 107 return address >= StaticCodeStart && address < StaticCodeStart + StaticCodeSize; 108 } 109 110 public ConcurrentQueue<(ulong address, FuncProfile funcProfile)> GetProfiledFuncsToTranslate(TranslatorCache<TranslatedFunction> funcs) 111 { 112 var profiledFuncsToTranslate = new ConcurrentQueue<(ulong address, FuncProfile funcProfile)>(); 113 114 foreach (var profiledFunc in ProfiledFuncs) 115 { 116 if (!funcs.ContainsKey(profiledFunc.Key)) 117 { 118 profiledFuncsToTranslate.Enqueue((profiledFunc.Key, profiledFunc.Value)); 119 } 120 } 121 122 return profiledFuncsToTranslate; 123 } 124 125 public void ClearEntries() 126 { 127 ProfiledFuncs.Clear(); 128 ProfiledFuncs.TrimExcess(); 129 } 130 131 public void PreLoad() 132 { 133 _lastHash = default; 134 135 string fileNameActual = $"{_ptc.CachePathActual}.info"; 136 string fileNameBackup = $"{_ptc.CachePathBackup}.info"; 137 138 FileInfo fileInfoActual = new(fileNameActual); 139 FileInfo fileInfoBackup = new(fileNameBackup); 140 141 if (fileInfoActual.Exists && fileInfoActual.Length != 0L) 142 { 143 if (!Load(fileNameActual, false)) 144 { 145 if (fileInfoBackup.Exists && fileInfoBackup.Length != 0L) 146 { 147 Load(fileNameBackup, true); 148 } 149 } 150 } 151 else if (fileInfoBackup.Exists && fileInfoBackup.Length != 0L) 152 { 153 Load(fileNameBackup, true); 154 } 155 } 156 157 private bool Load(string fileName, bool isBackup) 158 { 159 using (FileStream compressedStream = new(fileName, FileMode.Open)) 160 using (DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress, true)) 161 { 162 OuterHeader outerHeader = DeserializeStructure<OuterHeader>(compressedStream); 163 164 if (!outerHeader.IsHeaderValid()) 165 { 166 InvalidateCompressedStream(compressedStream); 167 168 return false; 169 } 170 171 if (outerHeader.Magic != _outerHeaderMagic) 172 { 173 InvalidateCompressedStream(compressedStream); 174 175 return false; 176 } 177 178 if (outerHeader.InfoFileVersion != InternalVersion && !_migrateInternalVersions.Contains(outerHeader.InfoFileVersion)) 179 { 180 InvalidateCompressedStream(compressedStream); 181 182 return false; 183 } 184 185 if (outerHeader.Endianness != Ptc.GetEndianness()) 186 { 187 InvalidateCompressedStream(compressedStream); 188 189 return false; 190 } 191 192 using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); 193 Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L); 194 195 try 196 { 197 deflateStream.CopyTo(stream); 198 } 199 catch 200 { 201 InvalidateCompressedStream(compressedStream); 202 203 return false; 204 } 205 206 Debug.Assert(stream.Position == stream.Length); 207 208 stream.Seek(0L, SeekOrigin.Begin); 209 210 Hash128 expectedHash = DeserializeStructure<Hash128>(stream); 211 212 Hash128 actualHash = XXHash128.ComputeHash(GetReadOnlySpan(stream)); 213 214 if (actualHash != expectedHash) 215 { 216 InvalidateCompressedStream(compressedStream); 217 218 return false; 219 } 220 221 switch (outerHeader.InfoFileVersion) 222 { 223 case InternalVersion: 224 ProfiledFuncs = Deserialize(stream); 225 break; 226 case 1866: 227 ProfiledFuncs = Deserialize(stream, (address, profile) => (address + 0x500000UL, profile)); 228 break; 229 default: 230 Logger.Error?.Print(LogClass.Ptc, $"No migration path for {nameof(outerHeader.InfoFileVersion)} '{outerHeader.InfoFileVersion}'. Discarding cache."); 231 InvalidateCompressedStream(compressedStream); 232 return false; 233 } 234 235 Debug.Assert(stream.Position == stream.Length); 236 237 _lastHash = actualHash; 238 } 239 240 long fileSize = new FileInfo(fileName).Length; 241 242 Logger.Info?.Print(LogClass.Ptc, $"{(isBackup ? "Loaded Backup Profiling Info" : "Loaded Profiling Info")} (size: {fileSize} bytes, profiled functions: {ProfiledFuncs.Count})."); 243 244 return true; 245 } 246 247 private static Dictionary<ulong, FuncProfile> Deserialize(Stream stream, Func<ulong, FuncProfile, (ulong, FuncProfile)> migrateEntryFunc = null) 248 { 249 if (migrateEntryFunc != null) 250 { 251 return DeserializeAndUpdateDictionary(stream, DeserializeStructure<FuncProfile>, migrateEntryFunc); 252 } 253 254 return DeserializeDictionary<ulong, FuncProfile>(stream, DeserializeStructure<FuncProfile>); 255 } 256 257 private static ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream) 258 { 259 return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position); 260 } 261 262 private static void InvalidateCompressedStream(FileStream compressedStream) 263 { 264 compressedStream.SetLength(0L); 265 } 266 267 private void PreSave(object source, ElapsedEventArgs e) 268 { 269 _waitEvent.Reset(); 270 271 string fileNameActual = $"{_ptc.CachePathActual}.info"; 272 string fileNameBackup = $"{_ptc.CachePathBackup}.info"; 273 274 FileInfo fileInfoActual = new(fileNameActual); 275 276 if (fileInfoActual.Exists && fileInfoActual.Length != 0L) 277 { 278 File.Copy(fileNameActual, fileNameBackup, true); 279 } 280 281 Save(fileNameActual); 282 283 _waitEvent.Set(); 284 } 285 286 private void Save(string fileName) 287 { 288 int profiledFuncsCount; 289 290 OuterHeader outerHeader = new() 291 { 292 Magic = _outerHeaderMagic, 293 294 InfoFileVersion = InternalVersion, 295 Endianness = Ptc.GetEndianness(), 296 }; 297 298 outerHeader.SetHeaderHash(); 299 300 using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) 301 { 302 Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L); 303 304 stream.Seek(Unsafe.SizeOf<Hash128>(), SeekOrigin.Begin); 305 306 lock (_lock) 307 { 308 Serialize(stream, ProfiledFuncs); 309 310 profiledFuncsCount = ProfiledFuncs.Count; 311 } 312 313 Debug.Assert(stream.Position == stream.Length); 314 315 stream.Seek(Unsafe.SizeOf<Hash128>(), SeekOrigin.Begin); 316 Hash128 hash = XXHash128.ComputeHash(GetReadOnlySpan(stream)); 317 318 stream.Seek(0L, SeekOrigin.Begin); 319 SerializeStructure(stream, hash); 320 321 if (hash == _lastHash) 322 { 323 return; 324 } 325 326 using FileStream compressedStream = new(fileName, FileMode.OpenOrCreate); 327 using DeflateStream deflateStream = new(compressedStream, SaveCompressionLevel, true); 328 try 329 { 330 SerializeStructure(compressedStream, outerHeader); 331 332 stream.WriteTo(deflateStream); 333 334 _lastHash = hash; 335 } 336 catch 337 { 338 compressedStream.Position = 0L; 339 340 _lastHash = default; 341 } 342 343 if (compressedStream.Position < compressedStream.Length) 344 { 345 compressedStream.SetLength(compressedStream.Position); 346 } 347 } 348 349 long fileSize = new FileInfo(fileName).Length; 350 351 if (fileSize != 0L) 352 { 353 Logger.Info?.Print(LogClass.Ptc, $"Saved Profiling Info (size: {fileSize} bytes, profiled functions: {profiledFuncsCount})."); 354 } 355 } 356 357 private static void Serialize(Stream stream, Dictionary<ulong, FuncProfile> profiledFuncs) 358 { 359 SerializeDictionary(stream, profiledFuncs, SerializeStructure); 360 } 361 362 [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 29*/)] 363 private struct OuterHeader 364 { 365 public ulong Magic; 366 367 public uint InfoFileVersion; 368 369 public bool Endianness; 370 371 public Hash128 HeaderHash; 372 373 public void SetHeaderHash() 374 { 375 Span<OuterHeader> spanHeader = MemoryMarshal.CreateSpan(ref this, 1); 376 377 HeaderHash = XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf<OuterHeader>() - Unsafe.SizeOf<Hash128>())]); 378 } 379 380 public bool IsHeaderValid() 381 { 382 Span<OuterHeader> spanHeader = MemoryMarshal.CreateSpan(ref this, 1); 383 384 return XXHash128.ComputeHash(MemoryMarshal.AsBytes(spanHeader)[..(Unsafe.SizeOf<OuterHeader>() - Unsafe.SizeOf<Hash128>())]) == HeaderHash; 385 } 386 } 387 388 [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)] 389 public struct FuncProfile 390 { 391 public ExecutionMode Mode; 392 public bool HighCq; 393 394 public FuncProfile(ExecutionMode mode, bool highCq) 395 { 396 Mode = mode; 397 HighCq = highCq; 398 } 399 } 400 401 public void Start() 402 { 403 if (_ptc.State == PtcState.Enabled || 404 _ptc.State == PtcState.Continuing) 405 { 406 Enabled = true; 407 408 _timer.Enabled = true; 409 } 410 } 411 412 public void Stop() 413 { 414 Enabled = false; 415 416 if (!_disposed) 417 { 418 _timer.Enabled = false; 419 } 420 } 421 422 public void Wait() 423 { 424 _waitEvent.WaitOne(); 425 } 426 427 public void Dispose() 428 { 429 if (!_disposed) 430 { 431 _disposed = true; 432 433 _timer.Elapsed -= PreSave; 434 _timer.Dispose(); 435 436 Wait(); 437 _waitEvent.Dispose(); 438 } 439 } 440 } 441 }