using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using Server.Accounting; using Server.Items; using Server.Mobiles; using Server.Multis; namespace Server.Custom { /// /// Measures the main-thread cost of every read the bridge plugin would perform, against /// whatever world is currently loaded. Read-only. Test scaffolding, not part of the bridge. /// /// Everything here runs on the Core thread, which is exactly where the real plugin's /// reads must run, so these timings are the ones that matter for frame budget. /// public static class BridgeProbe { private const int Iterations = 20; private static readonly AosAttribute[] AllAttrs = (AosAttribute[])Enum.GetValues(typeof(AosAttribute)); private static readonly AosWeaponAttribute[] AllWeaponAttrs = (AosWeaponAttribute[])Enum.GetValues(typeof(AosWeaponAttribute)); private static readonly AosArmorAttribute[] AllArmorAttrs = (AosArmorAttribute[])Enum.GetValues(typeof(AosArmorAttribute)); public static void Initialize() { if (Config.Get("Bridge.ProbeOnStart", false)) EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(2.0), Run); } private static void Log(string fmt, params object[] args) { Console.WriteLine("[BridgeProbe] " + String.Format(fmt, args)); } private static void Run() { try { var players = CollectSeededChars(); var houses = BaseHouse.AllHouses; var vendors = PlayerVendor.PlayerVendors ?? new List(); Log("world: {0} seeded chars, {1} houses, {2} vendors, {3} accounts", players.Count, houses.Count, vendors.Count, Accounting.Accounts.Count); Log("thread: {0} (id {1})", System.Threading.Thread.CurrentThread.Name, System.Threading.Thread.CurrentThread.ManagedThreadId); Log(""); // ---- one full character profile, the heaviest single read ---- var sb = new StringBuilder(8192); double perProfile = Time(() => { for (int i = 0; i < players.Count; i++) { sb.Clear(); WriteProfile(sb, players[i]); } }) / Math.Max(1, players.Count); sb.Clear(); if (players.Count > 0) WriteProfile(sb, players[0]); int profileBytes = sb.Length; Log("char.profile {0,8:F3} ms/char {1,6} bytes json -> {2:F1} ms for all {3}", perProfile, profileBytes, perProfile * players.Count, players.Count); // ---- vitals: the 30s sweep the doc proposes ---- double vitals = Time(() => { var b = new StringBuilder(256); for (int i = 0; i < players.Count; i++) { b.Clear(); WriteVitals(b, players[i]); } }); Log("vitals sweep {0,8:F3} ms for {1} chars ({2:F4} ms/char)", vitals, players.Count, vitals / Math.Max(1, players.Count)); // ---- house decay sweep (III.3) ---- double decay = Time(() => { for (int i = 0; i < houses.Count; i++) { var lvl = houses[i].DecayLevel; GC.KeepAlive(lvl); } }); Log("decay sweep {0,8:F3} ms for {1} houses ({2:F4} ms/house)", decay, houses.Count, decay / Math.Max(1, houses.Count)); // ---- economy: money supply snapshot ---- double econ = 0; double total = 0; econ = Time(() => { total = 0; foreach (Account a in Accounting.Accounts.GetAccounts()) total += a.TotalCurrency; }); Log("economy sweep {0,8:F3} ms for {1} accounts (supply {2:N0} gold)", econ, Accounting.Accounts.Count, total * Account.CurrencyThreshold); // ---- player vendor snapshot ---- int listings = 0; double vend = Time(() => { listings = 0; var b = new StringBuilder(4096); for (int i = 0; i < vendors.Count; i++) { b.Clear(); listings += WriteVendor(b, vendors[i]); } }); Log("vendor snap {0,8:F3} ms for {1} vendors ({2} listings)", vend, vendors.Count, listings); Log(""); Log("--- extrapolation (linear, same gear complexity) ---"); Log(" vitals sweep @ 200 online: {0,7:F2} ms", vitals / Math.Max(1, players.Count) * 200); Log(" vitals sweep @ 1000 online: {0,7:F2} ms", vitals / Math.Max(1, players.Count) * 1000); Log(" profiles for 1000 chars : {0,7:F1} ms <-- never do this in a sweep", perProfile * 1000); Log(" decay sweep @ 2000 houses: {0,7:F2} ms", decay / Math.Max(1, houses.Count) * 2000); Log(" economy @ 5000 accts : {0,7:F2} ms", econ / Math.Max(1, Accounting.Accounts.Count) * 5000); } catch (Exception ex) { Log("FAILED: " + ex); } } /// Best-of-N: the minimum is the least noisy estimate of true cost. private static double Time(Action action) { action(); // warm up JIT and caches double best = double.MaxValue; var sw = new Stopwatch(); for (int i = 0; i < Iterations; i++) { sw.Restart(); action(); sw.Stop(); double ms = sw.Elapsed.TotalMilliseconds; if (ms < best) best = ms; } return best; } private static List CollectSeededChars() { var list = new List(); foreach (Account a in Accounting.Accounts.GetAccounts()) { if (!a.Username.StartsWith("seed_", StringComparison.Ordinal)) continue; for (int i = 0; i < a.Length; i++) { var pm = a[i] as PlayerMobile; if (pm != null) list.Add(pm); } } return list; } private static void WriteVitals(StringBuilder sb, PlayerMobile m) { sb.Append("{\"kind\":\"char.vitals\",\"serial\":\"0x"); sb.Append(m.Serial.Value.ToString("X")); sb.Append("\",\"hits\":").Append(m.Hits); sb.Append(",\"hitsMax\":").Append(m.HitsMax); sb.Append(",\"mana\":").Append(m.Mana); sb.Append(",\"stam\":").Append(m.Stam); sb.Append(",\"str\":").Append(m.Str); sb.Append(",\"dex\":").Append(m.Dex); sb.Append(",\"int\":").Append(m.Int); sb.Append(",\"x\":").Append(m.X); sb.Append(",\"y\":").Append(m.Y); sb.Append(",\"online\":").Append(m.NetState != null ? "true" : "false"); sb.Append('}'); } private static void WriteProfile(StringBuilder sb, PlayerMobile m) { sb.Append("{\"kind\":\"char.profile\",\"serial\":\"0x"); sb.Append(m.Serial.Value.ToString("X")); sb.Append("\",\"name\":\"").Append(m.Name).Append('"'); sb.Append(",\"body\":").Append(m.Body.BodyID); sb.Append(",\"online\":").Append(m.NetState != null ? "true" : "false"); sb.Append(",\"stats\":{\"str\":").Append(m.Str); sb.Append(",\"dex\":").Append(m.Dex); sb.Append(",\"int\":").Append(m.Int); sb.Append(",\"hits\":").Append(m.Hits).Append(",\"hitsMax\":").Append(m.HitsMax); sb.Append(",\"mana\":").Append(m.Mana).Append(",\"manaMax\":").Append(m.ManaMax); sb.Append(",\"stam\":").Append(m.Stam).Append(",\"stamMax\":").Append(m.StamMax); sb.Append(",\"fame\":").Append(m.Fame).Append(",\"karma\":").Append(m.Karma); sb.Append(",\"luck\":").Append(m.Luck); sb.Append(",\"resist\":{\"phys\":").Append(m.PhysicalResistance); sb.Append(",\"fire\":").Append(m.FireResistance); sb.Append(",\"cold\":").Append(m.ColdResistance); sb.Append(",\"pois\":").Append(m.PoisonResistance); sb.Append(",\"energy\":").Append(m.EnergyResistance).Append("}}"); sb.Append(",\"skills\":["); bool first = true; for (int i = 0; i < m.Skills.Length; i++) { var s = m.Skills[i]; if (s.Base <= 0.0) continue; // untrained: the bridge should not ship ~50 zeroes per char if (!first) sb.Append(','); first = false; sb.Append("{\"n\":\"").Append(s.SkillName).Append('"'); sb.Append(",\"base\":").Append(s.Base.ToString("F1")); sb.Append(",\"value\":").Append(s.Value.ToString("F1")); sb.Append(",\"cap\":").Append(s.Cap.ToString("F1")); sb.Append(",\"lock\":\"").Append(s.Lock).Append("\"}"); } sb.Append(']'); sb.Append(",\"equipment\":["); first = true; foreach (var item in m.Items) { if (item.Layer == Layer.Backpack || item.Layer == Layer.Bank || item.Layer == Layer.Hair || item.Layer == Layer.FacialHair || item.Layer == Layer.Mount) continue; if (!first) sb.Append(','); first = false; sb.Append("{\"serial\":\"0x").Append(item.Serial.Value.ToString("X")); sb.Append("\",\"layer\":\"").Append(item.Layer).Append('"'); sb.Append(",\"itemId\":").Append(item.ItemID); sb.Append(",\"hue\":").Append(item.Hue); sb.Append(",\"cliloc\":").Append(item.LabelNumber); if (item.Name != null) sb.Append(",\"name\":\"").Append(item.Name).Append('"'); sb.Append(",\"mods\":{"); bool m1 = true; var weapon = item as BaseWeapon; var armor = item as BaseArmor; if (weapon != null) { sb.Append("\"minDamage\":").Append(weapon.MinDamage); sb.Append(",\"maxDamage\":").Append(weapon.MaxDamage); m1 = false; WriteAttrs(sb, weapon.Attributes, ref m1); WriteWeaponAttrs(sb, weapon.WeaponAttributes, ref m1); } else if (armor != null) { sb.Append("\"baseRating\":").Append(armor.BaseArmorRating); m1 = false; WriteAttrs(sb, armor.Attributes, ref m1); WriteArmorAttrs(sb, armor.ArmorAttributes, ref m1); } sb.Append("}}"); } sb.Append(']'); sb.Append('}'); } private static void WriteAttrs(StringBuilder sb, AosAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllAttrs.Length; i++) { int v = a[AllAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllAttrs[i]).Append("\":").Append(v); } } private static void WriteWeaponAttrs(StringBuilder sb, AosWeaponAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllWeaponAttrs.Length; i++) { int v = a[AllWeaponAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllWeaponAttrs[i]).Append("\":").Append(v); } } private static void WriteArmorAttrs(StringBuilder sb, AosArmorAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllArmorAttrs.Length; i++) { int v = a[AllArmorAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllArmorAttrs[i]).Append("\":").Append(v); } } private static int WriteVendor(StringBuilder sb, PlayerVendor v) { int count = 0; sb.Append("{\"kind\":\"vendor.snapshot\",\"serial\":\"0x"); sb.Append(v.Serial.Value.ToString("X")); sb.Append("\",\"holdGold\":").Append(v.HoldGold); sb.Append(",\"listings\":["); var pack = v.Backpack; if (pack != null) { bool first = true; foreach (var item in pack.Items) { var vi = v.GetVendorItem(item); if (vi == null) continue; if (!first) sb.Append(','); first = false; count++; sb.Append("{\"serial\":\"0x").Append(item.Serial.Value.ToString("X")); sb.Append("\",\"itemId\":").Append(item.ItemID); sb.Append(",\"price\":").Append(vi.Price); sb.Append(",\"forSale\":").Append(vi.IsForSale ? "true" : "false"); sb.Append('}'); } } sb.Append("]}"); return count; } } }