BridgeSeeder and BridgeProbe are not part of the bridge, but living under overlay/Scripts/Custom/ meant deploy.ps1 would copy them onto any server we deployed to. Move them to tools/scaffolding/, which nothing syncs. overlay/Config/Bridge.cfg now carries the bridge's own settings (host, port, queue cap, sweep intervals) rather than the scaffolding flags. Config.Get returns false for a missing key, so a deployed server never runs the seeder or probe even if their .cs files are present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
406 lines
14 KiB
C#
406 lines
14 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<PlayerVendor>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Best-of-N: the minimum is the least noisy estimate of true cost.</summary>
|
|
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<PlayerMobile> CollectSeededChars()
|
|
{
|
|
var list = new List<PlayerMobile>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|