// MonoGame - Copyright (C) The MonoGame Team // This file is subject to the terms and conditions defined in // file 'LICENSE.txt', which is part of this source code package. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Microsoft.Xna.Framework.Content.Pipeline { /// /// A collection of content building statistics for use in diagnosing content issues. /// public class ContentStatsCollection { private static readonly string _header = "Source File,Dest File,Processor Type,Content Type,Source File Size,Dest File Size,Build Seconds"; private static readonly Regex _split = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); private readonly object _locker = new object(); private readonly Dictionary _statsBySource = new Dictionary(1024); public static readonly string Extension = ".mgstats"; /// /// Optionally used for copying stats that were stored in another collection. /// public ContentStatsCollection PreviousStats { get; set; } /// /// The internal content statistics dictionary. /// public IReadOnlyDictionary Stats { get { return _statsBySource; } } /// /// Get the content statistics for a source file and returns true if found. /// public bool TryGetStats(string sourceFile, out ContentStats stats) { lock (_locker) { if (!_statsBySource.TryGetValue(sourceFile, out stats)) return false; return true; } } /// /// Clears all the content statistics. /// public void Reset() { lock (_locker) _statsBySource.Clear(); } /// /// Store content building stats for a source file. /// /// The absolute path to the source asset file. /// The absolute path to the destination content file. /// The type name of the content processor. /// The content type object. /// The build time in seconds. public void RecordStats(string sourceFile, string destFile, string processorType, Type contentType, float buildSeconds) { var sourceSize = new FileInfo(sourceFile).Length; var destSize = new FileInfo(destFile).Length; lock (_locker) { ContentStats stats; _statsBySource.TryGetValue(sourceFile, out stats); stats.SourceFile = sourceFile; stats.DestFile = destFile; stats.SourceFileSize = sourceSize; stats.DestFileSize = destSize; stats.ContentType = GetFriendlyTypeName(contentType); stats.ProcessorType = processorType; stats.BuildSeconds = buildSeconds; _statsBySource[stats.SourceFile] = stats; } } /// /// Copy content building stats to the current collection from the PreviousStats. /// /// The absolute path to the source asset file. public void CopyPreviousStats(string sourceFile) { if (PreviousStats == null) return; lock (_locker) { if (_statsBySource.ContainsKey(sourceFile)) return; ContentStats stats; if (PreviousStats.TryGetStats(sourceFile, out stats)) _statsBySource[stats.SourceFile] = stats; } } private static string GetFriendlyTypeName(Type type) { if (type == null) return ""; if (type == typeof(int)) return "int"; else if (type == typeof(short)) return "short"; else if (type == typeof(byte)) return "byte"; else if (type == typeof(bool)) return "bool"; else if (type == typeof(long)) return "long"; else if (type == typeof(float)) return "float"; else if (type == typeof(double)) return "double"; else if (type == typeof(decimal)) return "decimal"; else if (type == typeof(string)) return "string"; else if (type.IsArray) return GetFriendlyTypeName(type.GetElementType()) + "[" + new string(',', type.GetArrayRank() - 1) + "]"; else if (type.IsGenericType) return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(x => GetFriendlyTypeName(x)).ToArray()) + ">"; else return type.Name; } /// /// Load the content statistics from a folder. /// /// The folder where the .mgstats file can be found. /// Returns the content statistics or an empty collection. public static ContentStatsCollection Read(string outputPath) { var collection = new ContentStatsCollection(); var filePath = Path.Combine(outputPath, Extension); try { var lines = File.ReadAllLines(filePath); // The first line is the CSV header... if it doesn't match then // assume the data is invalid or changed formats. if (lines[0] != _header) return collection; for (var i = 1; i < lines.Length; i++) { var columns = _split.Split(lines[i]); if (columns.Length != 7) continue; ContentStats stats; stats.SourceFile = columns[0].Trim('"'); stats.DestFile = columns[1].Trim('"'); stats.ProcessorType = columns[2].Trim('"'); stats.ContentType = columns[3].Trim('"'); stats.SourceFileSize = long.Parse(columns[4]); stats.DestFileSize = long.Parse(columns[5]); stats.BuildSeconds = float.Parse(columns[6]); if (!collection._statsBySource.ContainsKey(stats.SourceFile)) collection._statsBySource.Add(stats.SourceFile, stats); } } catch (Exception ex) { // Assume the file didn't exist or was incorrectly // formatted... either way we start from fresh data. collection.Reset(); } return collection; } /// /// Write the content statistics to a folder with the .mgstats file name. /// /// The folder to write the .mgstats file. public void Write(string outputPath) { // ensure the output folder exists Directory.CreateDirectory(outputPath); var filePath = Path.Combine(outputPath, Extension); using (var textWriter = new StreamWriter(filePath, false, new UTF8Encoding(false))) { // Sort the items alphabetically to ensure a consistent output // and better mergability of the resulting file. var contentStats = _statsBySource.Values.OrderBy(c => c.SourceFile, StringComparer.InvariantCulture).ToList(); textWriter.WriteLine(_header); foreach (var stats in contentStats) textWriter.WriteLine("\"{0}\",\"{1}\",\"{2}\",\"{3}\",{4},{5},{6}", stats.SourceFile, stats.DestFile, stats.ProcessorType, stats.ContentType, stats.SourceFileSize, stats.DestFileSize, stats.BuildSeconds); } } /// /// Merge in statistics from PreviousStats that do not exist in this collection. /// public void MergePreviousStats() { if (PreviousStats == null) return; foreach (var stats in PreviousStats._statsBySource.Values) { if (!_statsBySource.ContainsKey(stats.SourceFile)) _statsBySource.Add(stats.SourceFile, stats); } } } }