From 109ccbb0b064f8f6d54a98f228fa46b98f050da4 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 9 Apr 2015 22:25:30 +0100 Subject: [PATCH 1/4] Added ConcurrentCache, a thread-safe Cache. --- OpenRA.Game/OpenRA.Game.csproj | 1 + OpenRA.Game/Primitives/Cache.cs | 12 ++---- OpenRA.Game/Primitives/ConcurrentCache.cs | 47 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 OpenRA.Game/Primitives/ConcurrentCache.cs diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index f3bfb5ba67..033a547c5e 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -144,6 +144,7 @@ + diff --git a/OpenRA.Game/Primitives/Cache.cs b/OpenRA.Game/Primitives/Cache.cs index da285fb560..8daf43cd7c 100644 --- a/OpenRA.Game/Primitives/Cache.cs +++ b/OpenRA.Game/Primitives/Cache.cs @@ -9,7 +9,6 @@ #endregion using System; -using System.Collections; using System.Collections.Generic; namespace OpenRA.Primitives @@ -23,6 +22,7 @@ namespace OpenRA.Primitives { if (loader == null) throw new ArgumentNullException("loader"); + this.loader = loader; cache = new Dictionary(c); } @@ -32,13 +32,7 @@ namespace OpenRA.Primitives public U this[T key] { - get - { - U result; - if (!cache.TryGetValue(key, out result)) - cache.Add(key, result = loader(key)); - return result; - } + get { return cache.GetOrAdd(key, loader); } } public bool ContainsKey(T key) { return cache.ContainsKey(key); } @@ -47,6 +41,6 @@ namespace OpenRA.Primitives public ICollection Keys { get { return cache.Keys; } } public ICollection Values { get { return cache.Values; } } public IEnumerator> GetEnumerator() { return cache.GetEnumerator(); } - IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } } diff --git a/OpenRA.Game/Primitives/ConcurrentCache.cs b/OpenRA.Game/Primitives/ConcurrentCache.cs new file mode 100644 index 0000000000..f717548469 --- /dev/null +++ b/OpenRA.Game/Primitives/ConcurrentCache.cs @@ -0,0 +1,47 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace OpenRA.Primitives +{ + public class ConcurrentCache : IReadOnlyDictionary + { + readonly ConcurrentDictionary cache; + readonly Func loader; + + public ConcurrentCache(Func loader, IEqualityComparer c) + { + if (loader == null) + throw new ArgumentNullException("loader"); + + this.loader = loader; + cache = new ConcurrentDictionary(c); + } + + public ConcurrentCache(Func loader) + : this(loader, EqualityComparer.Default) { } + + public U this[T key] + { + get { return cache.GetOrAdd(key, loader); } + } + + public bool ContainsKey(T key) { return cache.ContainsKey(key); } + public bool TryGetValue(T key, out U value) { return cache.TryGetValue(key, out value); } + public int Count { get { return cache.Count; } } + public ICollection Keys { get { return cache.Keys; } } + public ICollection Values { get { return cache.Values; } } + public IEnumerator> GetEnumerator() { return cache.GetEnumerator(); } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } + } +} From d2d2f4a8388ac95a62c26f7b1aec4810d3bc5d07 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 9 Apr 2015 22:26:33 +0100 Subject: [PATCH 2/4] Made FieldLoader thread-safe, and cache some expensive reflection calls. --- OpenRA.Game/FieldLoader.cs | 46 +++++++++++++++++++++++++------------- OpenRA.Game/ModData.cs | 3 +-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs index 086d3e959f..6c4bb59bb8 100644 --- a/OpenRA.Game/FieldLoader.cs +++ b/OpenRA.Game/FieldLoader.cs @@ -33,9 +33,17 @@ namespace OpenRA throw new NotImplementedException("FieldLoader: Missing field `{0}` on `{1}`".F(s, f.Name)); }; + static readonly ConcurrentCache TypeLoadInfo = + new ConcurrentCache(BuildTypeLoadInfo); + static readonly ConcurrentCache MemberHasTranslateAttribute = + new ConcurrentCache(member => member.HasAttribute()); + + static readonly object TranslationsLock = new object(); + static Dictionary translations; + public static void Load(object self, MiniYaml my) { - var loadInfo = typeLoadInfo[self.GetType()]; + var loadInfo = TypeLoadInfo[self.GetType()]; Dictionary md = null; @@ -82,7 +90,6 @@ namespace OpenRA return t; } - static readonly object[] NoIndexes = { }; public static void LoadField(object target, string key, string value) { const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; @@ -103,7 +110,7 @@ namespace OpenRA { var sa = prop.GetCustomAttributes(false).DefaultIfEmpty(SerializeAttribute.Default).First(); if (!sa.FromYamlKey) - prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), NoIndexes); + prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null); return; } @@ -162,7 +169,7 @@ namespace OpenRA } else if (fieldType == typeof(string)) { - if (field != null && field.HasAttribute()) + if (field != null && MemberHasTranslateAttribute[field]) return Regex.Replace(value, "@[^@]+@", m => Translate(m.Value.Substring(1, m.Value.Length - 2)), RegexOptions.Compiled); return value; } @@ -438,12 +445,10 @@ namespace OpenRA public static IEnumerable GetTypeLoadInfo(Type type, bool includePrivateByDefault = false) { - return typeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault)); + return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault)); } - static Cache> typeLoadInfo = new Cache>(BuildTypeLoadInfo); - - static List BuildTypeLoadInfo(Type type) + static FieldLoadInfo[] BuildTypeLoadInfo(Type type) { var ret = new List(); @@ -465,7 +470,7 @@ namespace OpenRA ret.Add(fli); } - return ret; + return ret.ToArray(); } [AttributeUsage(AttributeTargets.Field)] @@ -520,17 +525,27 @@ namespace OpenRA public static string Translate(string key) { - if (Translations == null || string.IsNullOrEmpty(key)) + if (string.IsNullOrEmpty(key)) return key; - string value; - if (!Translations.TryGetValue(key, out value)) - return key; + lock (TranslationsLock) + { + if (translations == null) + return key; - return value; + string value; + if (!translations.TryGetValue(key, out value)) + return key; + + return value; + } } - public static Dictionary Translations = new Dictionary(); + public static void SetTranslations(IDictionary translations) + { + lock (TranslationsLock) + FieldLoader.translations = new Dictionary(translations); + } } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] @@ -546,6 +561,7 @@ namespace OpenRA } // mirrors DescriptionAttribute from System.ComponentModel but we dont want to have to use that everywhere. + [AttributeUsage(AttributeTargets.All)] public sealed class DescAttribute : Attribute { public readonly string[] Lines; diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 58f4ed46ca..d0a2b037d6 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -116,7 +116,6 @@ namespace OpenRA if (!Manifest.Translations.Any()) { Languages = new string[0]; - FieldLoader.Translations = new Dictionary(); return; } @@ -144,7 +143,7 @@ namespace OpenRA translations.Add(tkv.Key, tkv.Value); } - FieldLoader.Translations = translations; + FieldLoader.SetTranslations(translations); } public Map PrepareMap(string uid) From 60238a858db38c7793c0269d698e2d5b64346340 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 9 Apr 2015 23:17:54 +0100 Subject: [PATCH 3/4] Load replays asynchronously and in parallel. This prevents the UI blocking, and also speeds up loading time for getting all the replays displayed. --- .../Widgets/Logic/ReplayBrowserLogic.cs | 160 ++++++++++-------- 1 file changed, 93 insertions(+), 67 deletions(-) diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs index adbcb007cb..622c43425d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs @@ -9,9 +9,12 @@ #endregion using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using OpenRA.FileFormats; using OpenRA.Mods.Common.Widgets; using OpenRA.Primitives; @@ -23,16 +26,17 @@ namespace OpenRA.Mods.Common.Widgets.Logic { static Filter filter = new Filter(); - Widget panel; - ScrollPanelWidget replayList, playerList; - ScrollItemWidget playerTemplate, playerHeader; - List replays; - Dictionary replayState = new Dictionary(); + readonly Widget panel; + readonly ScrollPanelWidget replayList, playerList; + readonly ScrollItemWidget playerTemplate, playerHeader; + readonly List replays = new List(); + readonly Dictionary replayState = new Dictionary(); + readonly Action onStart; Dictionary selectedSpawns; ReplayMetadata selectedReplay; - Action onStart; + volatile bool cancelLoadingReplays; [ObjectCreator.UseCtor] public ReplayBrowserLogic(Widget widget, Action onExit, Action onStart) @@ -46,7 +50,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic playerTemplate = playerList.Get("TEMPLATE"); playerList.RemoveChildren(); - panel.Get("CANCEL_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; + panel.Get("CANCEL_BUTTON").OnClick = () => { cancelLoadingReplays = true; Ui.CloseWindow(); onExit(); }; replayList = panel.Get("REPLAY_LIST"); var template = panel.Get("REPLAY_TEMPLATE"); @@ -54,26 +58,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic var mod = Game.ModData.Manifest.Mod; var dir = Platform.ResolvePath("^", "Replays", mod.Id, mod.Version); - replayList.RemoveChildren(); if (Directory.Exists(dir)) - { - using (new Support.PerfTimer("Load replays")) - { - replays = Directory - .GetFiles(dir, "*.orarep") - .Select(ReplayMetadata.Read) - .Where(r => r != null) - .OrderByDescending(r => r.GameInfo.StartTimeUtc) - .ToList(); - } - - foreach (var replay in replays) - AddReplay(replay, template); - - ApplyFilter(); - } - else - replays = new List(); + ThreadPool.QueueUserWorkItem(_ => LoadReplays(dir, template)); var watch = panel.Get("WATCH_BUTTON"); watch.IsDisabled = () => selectedReplay == null || selectedReplay.GameInfo.MapPreview.Status != MapStatus.Available; @@ -99,6 +85,40 @@ namespace OpenRA.Mods.Common.Widgets.Logic SetupManagement(); } + void LoadReplays(string dir, ScrollItemWidget template) + { + using (new Support.PerfTimer("Load replays")) + { + var loadedReplays = new ConcurrentBag(); + Parallel.ForEach(Directory.GetFiles(dir, "*.orarep"), (fileName, pls) => + { + if (cancelLoadingReplays) + { + pls.Stop(); + return; + } + + var replay = ReplayMetadata.Read(fileName); + if (replay != null) + loadedReplays.Add(replay); + }); + + if (cancelLoadingReplays) + return; + + var sortedReplays = loadedReplays.OrderByDescending(replay => replay.GameInfo.StartTimeUtc).ToList(); + Game.RunAfterTick(() => + { + replayList.RemoveChildren(); + foreach (var replay in sortedReplays) + AddReplay(replay, template); + + SetupReplayDependentFilters(); + ApplyFilter(); + }); + } + } + void SetupFilters() { // Game type @@ -202,6 +222,50 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } + // Outcome (depends on Player) + { + var ddb = panel.GetOrNull("FLT_OUTCOME_DROPDOWNBUTTON"); + if (ddb != null) + { + ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName); + + // Using list to maintain the order + var options = new List> + { + Pair.New(WinState.Undefined, ddb.GetText()), + Pair.New(WinState.Lost, "Defeat"), + Pair.New(WinState.Won, "Victory") + }; + var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); + + ddb.GetText = () => lookup[filter.Outcome]; + ddb.OnMouseDown = _ => + { + Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => + { + var item = ScrollItemWidget.Setup( + tpl, + () => filter.Outcome == option.First, + () => { filter.Outcome = option.First; ApplyFilter(); }); + item.Get("LABEL").GetText = () => option.Second; + return item; + }; + + ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem); + }; + } + } + + // Reset button + { + var button = panel.Get("FLT_RESET_BUTTON"); + button.IsDisabled = () => filter.IsEmpty; + button.OnClick = () => { filter = new Filter(); ApplyFilter(); }; + } + } + + void SetupReplayDependentFilters() + { // Map { var ddb = panel.GetOrNull("FLT_MAPNAME_DROPDOWNBUTTON"); @@ -258,40 +322,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - // Outcome (depends on Player) - { - var ddb = panel.GetOrNull("FLT_OUTCOME_DROPDOWNBUTTON"); - if (ddb != null) - { - ddb.IsDisabled = () => string.IsNullOrEmpty(filter.PlayerName); - - // Using list to maintain the order - var options = new List> - { - Pair.New(WinState.Undefined, ddb.GetText()), - Pair.New(WinState.Lost, "Defeat"), - Pair.New(WinState.Won, "Victory") - }; - var lookup = options.ToDictionary(kvp => kvp.First, kvp => kvp.Second); - - ddb.GetText = () => lookup[filter.Outcome]; - ddb.OnMouseDown = _ => - { - Func, ScrollItemWidget, ScrollItemWidget> setupItem = (option, tpl) => - { - var item = ScrollItemWidget.Setup( - tpl, - () => filter.Outcome == option.First, - () => { filter.Outcome = option.First; ApplyFilter(); }); - item.Get("LABEL").GetText = () => option.Second; - return item; - }; - - ddb.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 330, options, setupItem); - }; - } - } - // Faction (depends on Player) { var ddb = panel.GetOrNull("FLT_FACTION_DROPDOWNBUTTON"); @@ -323,13 +353,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic }; } } - - // Reset button - { - var button = panel.Get("FLT_RESET_BUTTON"); - button.IsDisabled = () => filter.IsEmpty; - button.OnClick = () => { filter = new Filter(); ApplyFilter(); }; - } } void SetupManagement() @@ -632,6 +655,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { Action startReplay = () => { + cancelLoadingReplays = true; Game.JoinReplay(selectedReplay.FilePath); Ui.CloseWindow(); onStart(); @@ -643,6 +667,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic void AddReplay(ReplayMetadata replay, ScrollItemWidget template) { + replays.Add(replay); + var item = ScrollItemWidget.Setup(template, () => selectedReplay == replay, () => SelectReplay(replay), From 087b407f468ba8647a94aad0c933825c2239b6d9 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 10 Apr 2015 00:12:18 +0100 Subject: [PATCH 4/4] Avoid ScrollPanelWidget drawing invisible child items. If child items in a scroll panel will be outside the scissor area, then we can avoid drawing them at all. If a scroll panel has many items, this reduces to the draw cost closer to those visible in the panel, rather than costing for all the items. --- OpenRA.Mods.Common/Widgets/ScrollPanelWidget.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OpenRA.Mods.Common/Widgets/ScrollPanelWidget.cs b/OpenRA.Mods.Common/Widgets/ScrollPanelWidget.cs index 41485f57e4..e0abd95317 100644 --- a/OpenRA.Mods.Common/Widgets/ScrollPanelWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ScrollPanelWidget.cs @@ -156,10 +156,13 @@ namespace OpenRA.Mods.Common.Widgets WidgetUtils.DrawRGBA(ChromeProvider.GetImage("scrollbar", downPressed || downDisabled ? "down_pressed" : "down_arrow"), new float2(downButtonRect.Left + downOffset, downButtonRect.Top + downOffset)); - Game.Renderer.EnableScissor(backgroundRect.InflateBy(-1, -1, -1, -1)); + var drawBounds = backgroundRect.InflateBy(-1, -1, -1, -1); + Game.Renderer.EnableScissor(drawBounds); + drawBounds.Offset((-ChildOrigin).ToPoint()); foreach (var child in Children) - child.DrawOuter(); + if (child.Bounds.IntersectsWith(drawBounds)) + child.DrawOuter(); Game.Renderer.DisableScissor(); }