Move test scaffolding out of the deploy path

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>
This commit is contained in:
2026-07-10 04:40:20 -05:00
parent 2c51bc720c
commit 6ab806e771
4 changed files with 84 additions and 10 deletions

View File

@@ -0,0 +1,405 @@
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;
}
}
}