Skip to content

Commit d064ef1

Browse files
authored
Merge pull request #9 from esnya/feature/improve-runtime-performance
zap: ⚡️ Improve runtime performance and serialized outputs
2 parents 2411c69 + 59229ab commit d064ef1

15 files changed

+629
-326
lines changed
Lines changed: 51 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,67 @@
11
using FrooxEngine;
2-
using Newtonsoft.Json;
3-
using Newtonsoft.Json.Converters;
4-
using System;
5-
using System.Collections.Generic;
6-
using System.Diagnostics;
7-
using System.Text;
2+
using System.Runtime.CompilerServices;
3+
using System.Text.Json.Serialization;
84

95
namespace ResoniteMetricsCounter.Metrics;
106

11-
internal enum MetricType
7+
/// <summary>
8+
/// Represents a metric that is associated with a specific target element.
9+
/// </summary>
10+
/// <typeparam name="T">The type of the target element, which must implement <see cref="IWorldElement"/>.</typeparam>
11+
public class Metric<T> where T : IWorldElement
1212
{
13-
PhysicsMoved,
14-
PhysicsUpdate,
15-
Updates,
16-
ProtoFluxContinuousChanges,
17-
ProtoFluxUpdates,
18-
Changes,
19-
Connectors,
20-
}
21-
22-
23-
#pragma warning disable CA1812
24-
internal sealed class SlotHierarchyConverter : JsonConverter<Slot>
25-
{
26-
private readonly Dictionary<Slot, string> cache = new();
27-
28-
public override Slot ReadJson(JsonReader reader, Type objectType, Slot? existingValue, bool hasExistingValue, JsonSerializer serializer)
13+
/// <summary>
14+
/// Target element of the metric.
15+
/// </summary>
16+
[JsonInclude] public T Target { get; private set; }
17+
18+
/// <summary>
19+
/// Ticks of the metric.
20+
/// </summary>
21+
[JsonInclude] public long Ticks { get; private set; }
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="Metric{T}"/> class.
25+
/// </summary>
26+
/// <param name="target">The target element of the metric.</param>
27+
/// <param name="ticks">The initial number of ticks. Default is 0.</param>
28+
public Metric(T target, long ticks = 0)
2929
{
30-
throw new NotImplementedException();
30+
Target = target;
31+
Ticks = ticks;
3132
}
3233

33-
public override void WriteJson(JsonWriter writer, Slot? value, JsonSerializer serializer)
34+
/// <summary>
35+
/// Adds the specified number of ticks to the metric.
36+
/// </summary>
37+
/// <param name="ticks">The number of ticks to add.</param>
38+
/// <throws><see cref="OverflowException"/> if the result is greater than <see cref="long.MaxValue"/>.</throws>
39+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
40+
public void Add(long ticks)
3441
{
35-
if (value is null)
36-
{
37-
writer.WriteNull();
38-
return;
39-
}
40-
41-
if (cache.TryGetValue(value, out var cached))
42-
{
43-
writer.WriteValue(cached);
44-
return;
45-
}
46-
47-
var stringBuilder = new StringBuilder();
48-
49-
for (var s = value; s != null; s = s.Parent)
50-
{
51-
stringBuilder.Insert(0, s.Name);
52-
stringBuilder.Insert(0, "/");
53-
}
54-
55-
writer.WriteValue(cache[value] = stringBuilder.ToString());
42+
Ticks += ticks;
5643
}
5744
}
58-
#pragma warning restore CA1812
5945

60-
internal struct Metric
46+
/// <summary>
47+
/// Represents a metric that is associated with a specific world refresh stage.
48+
/// </summary>
49+
/// <typeparam name="T">The type of the target element, which must implement <see cref="IWorldElement"/>.</typeparam>
50+
public sealed class StageMetric<T> : Metric<T> where T : IWorldElement
6151
{
62-
[JsonConverter(typeof(SlotHierarchyConverter))]
63-
public Slot Slot;
64-
65-
public string Name;
66-
67-
[JsonConverter(typeof(StringEnumConverter))]
68-
public MetricType Type;
69-
70-
public long Ticks;
71-
72-
public static long Frequency => Stopwatch.Frequency;
73-
74-
75-
[JsonConverter(typeof(SlotHierarchyConverter))]
76-
public readonly Slot? ObjectRoot
77-
{
78-
get
79-
{
80-
return Slot.Parent?.GetObjectRoot();
81-
}
82-
}
83-
84-
public readonly string Label
85-
{
86-
get
87-
{
88-
var parent = Slot.Parent;
89-
var root = parent?.GetObjectRoot();
90-
91-
if (parent is null)
92-
{
93-
return $"{Slot.Name}.{Name}[{Type}]";
94-
}
95-
else if (root is null || root == parent)
96-
{
97-
return $"{parent.Name}/{Slot.Name}.{Name}[{Type}]";
98-
}
99-
100-
return $"{root.Name}/../{parent.Name}/{Slot.Name}.{Name}[{Type}]";
101-
}
102-
}
103-
104-
public override readonly int GetHashCode()
105-
{
106-
return Slot.ReferenceID.GetHashCode() ^ Name.GetHashCode() ^ Type.GetHashCode();
107-
}
108-
109-
public static Metric operator +(Metric a, Metric b)
52+
/// <summary>
53+
/// World reflesh stage of the metric.
54+
/// </summary>
55+
[JsonInclude] public World.RefreshStage Stage { get; private set; }
56+
57+
/// <summary>
58+
/// Initializes a new instance of the <see cref="StageMetric{T}"/> class.
59+
/// </summary>
60+
/// <param name="stage">The world refresh stage of the metric.</param>
61+
/// <param name="target">The target element of the metric.</param>
62+
/// <param name="ticks">The initial number of ticks. Default is 0.</param>
63+
public StageMetric(World.RefreshStage stage, T target, long ticks = 0) : base(target, ticks)
11064
{
111-
return new Metric
112-
{
113-
Slot = a.Slot,
114-
Name = a.Name,
115-
Type = a.Type,
116-
Ticks = a.Ticks + b.Ticks,
117-
};
65+
Stage = stage;
11866
}
11967
}
Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,124 @@
11
using Elements.Core;
22
using FrooxEngine;
3-
using Newtonsoft.Json;
3+
using FrooxEngine.ProtoFlux;
4+
using ResoniteMetricsCounter.Serialization;
5+
using ResoniteMetricsCounter.Utils;
6+
using ResoniteModLoader;
47
using System;
58
using System.Collections.Generic;
69
using System.IO;
710
using System.Linq;
811
using System.Runtime.CompilerServices;
12+
using System.Text.Json;
13+
using System.Text.Json.Serialization;
914

1015
namespace ResoniteMetricsCounter.Metrics;
1116

12-
internal sealed class MetricsCounter : IDisposable
17+
18+
public sealed class MetricsCounter : IDisposable
1319
{
14-
internal Dictionary<int, Metric> Metrics = new();
15-
private readonly string filename;
16-
private HashSet<string> blackList;
17-
private Slot? ignoredHierarchy;
18-
internal long TotalTicks
20+
private readonly CachedElementValue<IWorldElement, bool> shouldSkip;
21+
22+
[JsonInclude] public Slot? IgnoredHierarchy { get; private set; }
23+
internal bool IsDisposed { get; private set; }
24+
25+
[JsonInclude] public string Filename { get; private set; }
26+
[JsonInclude] public VersionNumber EngineVersion { get; private set; }
27+
[JsonInclude] public HashSet<string> BlackList { get; private set; }
28+
29+
[JsonInclude] public MetricsByStageStorage<IWorldElement> ByElement { get; private set; } = new();
30+
[JsonInclude] public MetricsStorage<Slot> ByObjectRoot { get; private set; } = new();
31+
32+
public MetricsCounter(IEnumerable<string> blackList)
1933
{
20-
get;
21-
private set;
34+
shouldSkip = new(ShouldSkipImpl);
35+
36+
EngineVersion = Engine.Version;
37+
Filename = UniLog.GenerateLogName(EngineVersion.ToString(), "-trace").Replace(".log", ".json");
38+
BlackList = blackList.ToHashSet();
2239
}
23-
internal long MaxTicks
40+
41+
private bool ShouldSkipImpl(IWorldElement element)
2442
{
25-
get;
26-
private set;
43+
if (element.World.Focus != World.WorldFocus.Focused) return true;
44+
if (element.IsLocalElement || element.IsRemoved || BlackList.Contains(element.GetNameFast())) return true;
45+
46+
var slot = element.GetSlotFast();
47+
if (slot is null || IgnoredHierarchy is null) return false;
48+
return IgnoredHierarchy.IsChildOf(slot, includeSelf: true);
2749
}
2850

29-
public MetricsCounter(IEnumerable<string> blackList)
51+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
52+
public void AddForCurrentStage(object? obj, long ticks)
3053
{
31-
filename = UniLog.GenerateLogName(Engine.VersionNumber, "-trace").Replace(".log", ".json");
32-
this.blackList = blackList.ToHashSet();
54+
if (obj is IWorldElement element) AddForCurrentStage(element, ticks);
55+
else if (obj is ProtoFluxNodeGroup group) AddForCurrentStage(group, ticks);
56+
else if (ResoniteMod.IsDebugEnabled()) ResoniteMod.Debug($"Unknown object type: {obj?.GetType()}");
3357
}
3458

3559
[MethodImpl(MethodImplOptions.AggressiveInlining)]
36-
public void Add(string name, Slot slot, long ticks, MetricType type)
60+
private void AddForCurrentStage(ProtoFluxNodeGroup group, long ticks)
3761
{
38-
Add(new Metric()
39-
{
40-
Slot = slot,
41-
Name = name,
42-
Ticks = ticks,
43-
Type = type
44-
});
62+
var world = group.World;
63+
if (world.Focus != World.WorldFocus.Focused) return;
64+
65+
var node = group.Nodes.FirstOrDefault();
66+
if (node is null) return;
67+
68+
AddForCurrentStage(node, ticks);
4569
}
4670

47-
public void Add(in Metric metric)
71+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
72+
private void AddForCurrentStage(IWorldElement element, long ticks)
4873
{
49-
if (metric.Ticks == 0 || blackList.Contains(metric.Name) || (ignoredHierarchy is not null && metric.Slot.IsChildOf(ignoredHierarchy, includeSelf: true))) return;
74+
if (shouldSkip.GetOrCache(element)) return;
5075

51-
TotalTicks += metric.Ticks;
76+
ByElement.Add(element, ticks);
5277

53-
var id = metric.GetHashCode();
54-
if (Metrics.TryGetValue(id, out var prevValue))
55-
{
56-
Metrics[id] = prevValue + metric;
57-
}
58-
else
59-
{
60-
Metrics[id] = metric;
61-
}
78+
var objectRoot = element.GetExactObjectRootOrWorldRootFast();
79+
if (objectRoot is null) return;
6280

63-
var ticks = Metrics[id].Ticks;
64-
if (ticks > MaxTicks)
65-
{
66-
MaxTicks = ticks;
67-
}
81+
ByObjectRoot.Add(objectRoot, ticks);
6882
}
6983

84+
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
85+
{
86+
WriteIndented = true,
87+
IgnoreReadOnlyFields = false,
88+
IgnoreReadOnlyProperties = false,
89+
Converters = { new IWorldElementConverter(), new JsonStringEnumConverter<World.RefreshStage>() },
90+
};
91+
7092
public void Flush()
7193
{
72-
var serializer = new JsonSerializer();
73-
var streamWriter = new StreamWriter(filename, append: true);
74-
serializer.Serialize(streamWriter, Metrics);
75-
streamWriter.Close();
94+
ResoniteMod.DebugFunc(() => $"Writing metrics to {Filename}");
95+
using (var writer = new FileStream(Filename, FileMode.Create))
96+
{
97+
JsonSerializer.Serialize(writer, this, jsonSerializerOptions);
98+
}
7699
}
77100

78101
public void Dispose()
79102
{
103+
IsDisposed = true;
80104
Flush();
81105
}
82106

83107
internal void UpdateBlacklist(IEnumerable<string> blackList)
84108
{
85-
this.blackList = blackList.ToHashSet();
86-
foreach (var metric in Metrics.Values)
87-
{
88-
if (this.blackList.Contains(metric.Name))
89-
{
90-
Metrics.Remove(metric.GetHashCode());
91-
}
92-
}
109+
shouldSkip.Clear();
110+
BlackList = blackList.ToHashSet();
111+
ByElement.RemoveWhere(m => BlackList.Contains(m.Target.GetNameFast()));
112+
ByObjectRoot.RemoveWhere(m => BlackList.Contains(m.Target.GetNameFast()));
93113
}
94114

95-
internal void Remove(in Metric metric)
115+
internal void Remove(Slot slot)
96116
{
97-
Metrics.Remove(metric.GetHashCode());
117+
ByObjectRoot.Remove(slot);
98118
}
99119

100120
internal void IgnoreHierarchy(Slot slot)
101121
{
102-
ignoredHierarchy = slot;
122+
IgnoredHierarchy = slot;
103123
}
104124
}

0 commit comments

Comments
 (0)