v1.2.3.b8 - fix: perf percentiles and duplicate entity log lines
- #### Debug
- **PerfLog** - `p50`, `p95` and `p99` returned the upper bound of the histogram bucket instead of a real sample, so they could report a value higher than `max`. It happened in 74% of the windows in a 6.5 hour session (e.g. `avg=0.06ms p99=8.00ms max=4.68ms`). Percentiles are now taken from the actual samples of the window, and the bucket estimate is only used as a fallback for windows that overflow the buffer - interpolated inside the bucket and clamped to `max`.
- **EntityManager** - An entity that was already tracked logged a second `+` line, so creates and destroys stopped matching and the entity log read like a leak. Re-registering now logs `~ oldType -> newType` when the type changes and nothing when it does not.
- **Ghost, Ninja, C4Camouflage** - The hiding prop was registered twice, first as `prop_dynamic` and then as `empty_prop`. It is now tracked under the right name from creation through the new `trackAs` parameter of `CreateTrackedDynamicProp`.
This commit is contained in:
parent
b95c52028e
commit
68d311e130
8 changed files with 59 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -30,6 +30,7 @@ project.fragment.lock.json
|
||||||
*.userosscache
|
*.userosscache
|
||||||
|
|
||||||
# Logs, caches, temp
|
# Logs, caches, temp
|
||||||
|
log/
|
||||||
*.log
|
*.log
|
||||||
debug_*.txt
|
debug_*.txt
|
||||||
perf_*.txt
|
perf_*.txt
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using src.utils;
|
using src.utils;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using static src.jRandomSkills;
|
using static src.jRandomSkills;
|
||||||
|
|
@ -49,35 +49,72 @@ namespace src.player
|
||||||
|
|
||||||
private static readonly double[] bucketBoundsMs = [0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8, 16, 32, double.MaxValue];
|
private static readonly double[] bucketBoundsMs = [0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8, 16, 32, double.MaxValue];
|
||||||
|
|
||||||
|
private const int maxRawSamples = 4096;
|
||||||
|
|
||||||
private sealed class Aggregate
|
private sealed class Aggregate
|
||||||
{
|
{
|
||||||
public double TotalMs;
|
public double TotalMs;
|
||||||
public double MaxMs;
|
public double MaxMs;
|
||||||
public int Count;
|
public int Count;
|
||||||
public readonly int[] Buckets = new int[bucketBoundsMs.Length];
|
public readonly int[] Buckets = new int[bucketBoundsMs.Length];
|
||||||
|
public readonly List<double> Raw = [];
|
||||||
public DateTime WindowStart = DateTime.Now;
|
public DateTime WindowStart = DateTime.Now;
|
||||||
|
|
||||||
|
private bool rawSorted;
|
||||||
|
|
||||||
public void Add(double ms)
|
public void Add(double ms)
|
||||||
{
|
{
|
||||||
TotalMs += ms;
|
TotalMs += ms;
|
||||||
Count++;
|
Count++;
|
||||||
if (ms > MaxMs) MaxMs = ms;
|
if (ms > MaxMs) MaxMs = ms;
|
||||||
|
|
||||||
|
if (Raw.Count < maxRawSamples) Raw.Add(ms);
|
||||||
|
|
||||||
for (int i = 0; i < bucketBoundsMs.Length; i++)
|
for (int i = 0; i < bucketBoundsMs.Length; i++)
|
||||||
if (ms <= bucketBoundsMs[i]) { Buckets[i]++; break; }
|
if (ms <= bucketBoundsMs[i]) { Buckets[i]++; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Percentile(double fraction)
|
public double Percentile(double fraction)
|
||||||
|
{
|
||||||
|
if (Count == 0) return 0;
|
||||||
|
|
||||||
|
double value = Raw.Count == Count ? ExactPercentile(fraction) : BucketPercentile(fraction);
|
||||||
|
return value > MaxMs ? MaxMs : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double ExactPercentile(double fraction)
|
||||||
|
{
|
||||||
|
if (!rawSorted)
|
||||||
|
{
|
||||||
|
Raw.Sort();
|
||||||
|
rawSorted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = (int)Math.Ceiling(Raw.Count * fraction) - 1;
|
||||||
|
if (index < 0) index = 0;
|
||||||
|
if (index >= Raw.Count) index = Raw.Count - 1;
|
||||||
|
return Raw[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private double BucketPercentile(double fraction)
|
||||||
{
|
{
|
||||||
int target = (int)Math.Ceiling(Count * fraction);
|
int target = (int)Math.Ceiling(Count * fraction);
|
||||||
if (target < 1) target = 1;
|
if (target < 1) target = 1;
|
||||||
|
|
||||||
int seen = 0;
|
int seen = 0;
|
||||||
|
double lower = 0;
|
||||||
for (int i = 0; i < bucketBoundsMs.Length; i++)
|
for (int i = 0; i < bucketBoundsMs.Length; i++)
|
||||||
{
|
{
|
||||||
|
double upper = bucketBoundsMs[i] == double.MaxValue || bucketBoundsMs[i] > MaxMs ? MaxMs : bucketBoundsMs[i];
|
||||||
|
|
||||||
|
if (seen + Buckets[i] >= target)
|
||||||
|
{
|
||||||
|
if (Buckets[i] <= 0) return upper;
|
||||||
|
return lower + (upper - lower) * ((target - seen) / (double)Buckets[i]);
|
||||||
|
}
|
||||||
|
|
||||||
seen += Buckets[i];
|
seen += Buckets[i];
|
||||||
if (seen >= target)
|
lower = upper;
|
||||||
return bucketBoundsMs[i] == double.MaxValue ? MaxMs : bucketBoundsMs[i];
|
|
||||||
}
|
}
|
||||||
return MaxMs;
|
return MaxMs;
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +125,8 @@ namespace src.player
|
||||||
MaxMs = 0;
|
MaxMs = 0;
|
||||||
Count = 0;
|
Count = 0;
|
||||||
Array.Clear(Buckets);
|
Array.Clear(Buckets);
|
||||||
|
Raw.Clear();
|
||||||
|
rawSorted = false;
|
||||||
WindowStart = DateTime.Now;
|
WindowStart = DateTime.Now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Core.Attributes;
|
using CounterStrikeSharp.API.Core.Attributes;
|
||||||
using CounterStrikeSharp.API.Modules.Utils;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
|
|
@ -170,7 +170,7 @@ namespace src.player.skills
|
||||||
{
|
{
|
||||||
if (player == null || !player.IsValid) return;
|
if (player == null || !player.IsValid) return;
|
||||||
|
|
||||||
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index);
|
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index, trackAs: "empty_prop");
|
||||||
if (emptyProp == null || !emptyProp.IsValid) return;
|
if (emptyProp == null || !emptyProp.IsValid) return;
|
||||||
|
|
||||||
var playerPawn = player.PlayerPawn?.Value;
|
var playerPawn = player.PlayerPawn?.Value;
|
||||||
|
|
@ -188,7 +188,6 @@ namespace src.player.skills
|
||||||
|
|
||||||
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
||||||
|
|
||||||
EntityManager.RegisterExisting(emptyProp, player.Index, "empty_prop");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PlayerHurt(EventPlayerHurt @event)
|
public static void PlayerHurt(EventPlayerHurt @event)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Core.Attributes;
|
using CounterStrikeSharp.API.Core.Attributes;
|
||||||
using CounterStrikeSharp.API.Modules.Utils;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
|
|
@ -114,7 +114,7 @@ namespace src.player.skills
|
||||||
{
|
{
|
||||||
if (player == null || !player.IsValid) return;
|
if (player == null || !player.IsValid) return;
|
||||||
|
|
||||||
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index);
|
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index, trackAs: "empty_prop");
|
||||||
if (emptyProp == null || !emptyProp.IsValid) return;
|
if (emptyProp == null || !emptyProp.IsValid) return;
|
||||||
|
|
||||||
var playerPawn = player.PlayerPawn?.Value;
|
var playerPawn = player.PlayerPawn?.Value;
|
||||||
|
|
@ -132,7 +132,6 @@ namespace src.player.skills
|
||||||
|
|
||||||
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
||||||
|
|
||||||
EntityManager.RegisterExisting(emptyProp, player.Index, "empty_prop");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DisableSkill(CCSPlayerController player)
|
public static void DisableSkill(CCSPlayerController player)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Core.Attributes;
|
using CounterStrikeSharp.API.Core.Attributes;
|
||||||
using CounterStrikeSharp.API.Modules.Utils;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
|
|
@ -143,7 +143,7 @@ namespace src.player.skills
|
||||||
{
|
{
|
||||||
if (player == null || !player.IsValid) return;
|
if (player == null || !player.IsValid) return;
|
||||||
|
|
||||||
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index);
|
var emptyProp = EntityManager.CreateTrackedDynamicProp(player.Index, trackAs: "empty_prop");
|
||||||
if (emptyProp == null || !emptyProp.IsValid) return;
|
if (emptyProp == null || !emptyProp.IsValid) return;
|
||||||
|
|
||||||
var playerPawn = player.PlayerPawn?.Value;
|
var playerPawn = player.PlayerPawn?.Value;
|
||||||
|
|
@ -161,7 +161,6 @@ namespace src.player.skills
|
||||||
|
|
||||||
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");
|
||||||
|
|
||||||
EntityManager.RegisterExisting(emptyProp, player.Index, "empty_prop");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateNinja(CCSPlayerController? player)
|
private static void UpdateNinja(CCSPlayerController? player)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
||||||
using CounterStrikeSharp.API.Modules.Utils;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
|
|
@ -42,6 +42,8 @@ namespace src.utils
|
||||||
{
|
{
|
||||||
if (entityIndex == 0) return;
|
if (entityIndex == 0) return;
|
||||||
|
|
||||||
|
bool alreadyTracked = trackedEntities.TryGetValue(entityIndex, out var previous);
|
||||||
|
|
||||||
trackedEntities[entityIndex] = new EntityData
|
trackedEntities[entityIndex] = new EntityData
|
||||||
{
|
{
|
||||||
EntityIndex = entityIndex,
|
EntityIndex = entityIndex,
|
||||||
|
|
@ -50,8 +52,12 @@ namespace src.utils
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Config.DebugEnabled(DebugCategory.Entity))
|
if (!Config.DebugEnabled(DebugCategory.Entity)) return;
|
||||||
|
|
||||||
|
if (!alreadyTracked)
|
||||||
Debug.WriteToDebug($"[Entity] + {entityType} #{entityIndex} owner={DescribeOwner(playerIndex)} tracked={trackedEntities.Count}", DebugCategory.Entity);
|
Debug.WriteToDebug($"[Entity] + {entityType} #{entityIndex} owner={DescribeOwner(playerIndex)} tracked={trackedEntities.Count}", DebugCategory.Entity);
|
||||||
|
else if (previous.EntityType != entityType)
|
||||||
|
Debug.WriteToDebug($"[Entity] ~ {previous.EntityType} -> {entityType} #{entityIndex} owner={DescribeOwner(playerIndex)} tracked={trackedEntities.Count}", DebugCategory.Entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string DescribeOwner(uint playerIndex)
|
private static string DescribeOwner(uint playerIndex)
|
||||||
|
|
@ -130,7 +136,7 @@ namespace src.utils
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CDynamicProp? CreateTrackedDynamicProp(uint playerIndex, string designerName = "prop_dynamic")
|
public static CDynamicProp? CreateTrackedDynamicProp(uint playerIndex, string designerName = "prop_dynamic", string? trackAs = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -138,7 +144,7 @@ namespace src.utils
|
||||||
var prop = Utilities.CreateEntityByName<CDynamicProp>(designerName);
|
var prop = Utilities.CreateEntityByName<CDynamicProp>(designerName);
|
||||||
if (prop == null || !prop.IsValid) return null;
|
if (prop == null || !prop.IsValid) return null;
|
||||||
|
|
||||||
RegisterEntity(prop.Index, playerIndex, designerName);
|
RegisterEntity(prop.Index, playerIndex, trackAs ?? designerName);
|
||||||
return prop;
|
return prop;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue