/ src / ARMeilleure / Translation / PTC / PtcProfiler.cs
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  }