Skip to content

feat: Adding in Performance Tick Data that stores data for external consumption (RFC #7) #491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 26, 2021

Conversation

kvassall-unity
Copy link
Contributor

@kvassall-unity kvassall-unity commented Feb 22, 2021

@@ -853,6 +860,15 @@ private void NetworkUpdate()
currentNetworkTimeOffset += Mathf.Clamp(networkTimeOffset - currentNetworkTimeOffset, -maxDelta, maxDelta);
}
}

var profileTransport = NetworkConfig.NetworkTransport as ITransportProfilerData;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is totally fine but I always like to share newer c# syntax, this could also be
if(NetworkConfig.NetworkTransport is ITransportProfilerData profileTransport)

(tbh I'm still not 100% sold on the concept of inline variable declarations, it's been growing on me)

{
public interface ITransportProfilerData
{
Dictionary<string, int> GetTransportGetData();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused by this method name. Should it be something like GetTransportProfilerData ?


namespace MLAPI.Profiling
{
public static class PerformanceDataManager
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this class be internal? Do we ever expect external (non-MLAPI) code to call these APIs?

If we do, I think we should at least make BeginNewTick, AddTransportData, and GetData to be internal at the member level


public static void BeginNewTick()
{
s_ProfilerData = new PerformanceTickData
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to solve, but this is going to allocate on the heap for every network tick. I could see re-using problematic since we shared this data out and who knows whether consumers hold a reference to it / process async. But I'm curious if you or any other reviewers have any thoughts on ways to avoid an alloc here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we absolutely have to do an allocation a pool might help

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to Matt's point and also turning things into structs as much as we can might help a little on GC pressure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also hashing and caching strings might help a little more on string allocation but I'm not sure how applicable these options are here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we went with the generic approach, it needs to exist on the heap (dictionary<,>) since it's an indeterminate size. Although we should definitely make the containers for the heap objects structs when possible to reduce it

@@ -0,0 +1,21 @@
namespace MLAPI.Profiling
{
public enum ProfilerConstants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not convinced about using an enum here instead of string constants - why bother using a Dictionary, if we're restricting the set of values anyways? I think we should go one way or the other, either make it an explicit data set, or make it extensible. Right now we're taking all the cost of a Dictionary (loose abstraction, heap alloc) and none of the benefits (extensibility)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree with what Beck says here. However, this can offer a clear distinction between standard and custom data to profile. Not sure if we're aiming for such support right now.

{
s_ProfilerData = new PerformanceTickData
{
tickID = s_TickID++,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the odds are very low, but there's a risk of overflowing. We should at least use an unsigned. Probably unsigned long? No sense in having a negative tick id.

@@ -0,0 +1,21 @@
namespace MLAPI.Profiling
{
public enum ProfilerConstants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree with what Beck says here. However, this can offer a clear distinction between standard and custom data to profile. Not sure if we're aiming for such support right now.

m_Dictionary.Add(fieldName, value);
}

public void Increment(string fieldName, int count = 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just make the type ProfilerConstants and avoid the ToString()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IT cant be typed. Each transport will have its own types of strings that it wants to propagate up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly enough, we had the same thing in de-string-ifying the channels in MLAPI. They too have per-transport customization, so we still established a set of enums in the SDK with a 'LAST_ENTRY' enum, but then if your transport wants more than that you just do LAST_ENTRY + 1, etc. That let us have our type safety / low overhead but also be extensible. We do this in Transport.cs:8 and then like in TransportTest.cs:42

@kvassall-unity kvassall-unity changed the title feat: Performance Data Propagation (RFC #7) feat: Adding in Performance Tick Data that stores data for external consumption (RFC #7) Feb 24, 2021
@kvassall-unity kvassall-unity force-pushed the feature/performance_data_propagation branch from 83add46 to ed7b4a4 Compare February 24, 2021 20:21
Copy link
Contributor

@becksebenius-unity becksebenius-unity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close to approving but we need to address the enum.ToString() issue because as Matt calls out this will cause a lot of runtime allocations. I'd suggest changing them to string constants instead of using an enum.


public void AddNonDuplicateData(IReadOnlyDictionary<string, int> transportProfilerData)
{
IEnumerable<KeyValuePair<string, int>> nonDuplicates = transportProfilerData.Where(entry => !m_TickData.HasData(entry.Key));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this linq query is unnecessary here. you could do the HasData check inside the loop and avoid the linq cost

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://stackoverflow.com/questions/4044400/linq-performance-faq

The allocations and performance AFAIK are the same. nonDuplicates isn't an allocation and we probably save speed by iterating over less entries.
That is to say, the line in question doesn't get evaluated until the bottom for loop lazily. And I feel that this reads better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost all Linq queries, including .Where, allocate an IEnumerable on the heap to wrap the iteration of the collection. this line also allocates an anonymous function - it can't be static since it captures the member field m_TickData

I won't hold approval for this since it's not a high frequency alloc but I do think it could be optimized

Comment on lines 75 to 77
public delegate void PerformanceDataEventHandler(PerformanceTickData profilerData);

public event PerformanceDataEventHandler OnPerformanceDataEvent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#if UNITY_EDITOR || DEVELOPMENT_BUILD maybe? I wonder how are we planning to strip-out performance profiling stuff for non-Editor, non-Development targets, maybe I'm missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For performance reporting, I don't think we want to. The profiler itself should be stripped out in non-profiler but the data output itself is potentially relevant for end-user tools at runtime (for example, the net stats overlay). That said, users may still want to strip it out for performance reasons, but I think that should be a runtime check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm doing 1-to-1 parity right now. so since these weren't previously in an #if then i'm not doing that in this PR.

Ideally we could ifdef all instances of this class if we want.

@kvassall-unity kvassall-unity force-pushed the feature/performance_data_propagation branch from 957151b to 0529ef4 Compare February 26, 2021 16:36
@@ -769,6 +769,7 @@ internal static void HandleNetworkedVarDeltas(List<INetworkedVar> networkedVarLi
long readStartPos = stream.Position;

networkedVarList[i].ReadDelta(stream, IsServer);
PerformanceDataManager.Increment(ProfilerConstants.NumberNetworkVarsReceived.ToString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ToString() is being called on a string :)

Copy link
Contributor

@mattwalsh-unity mattwalsh-unity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly there are a bunch of cases where we're calling ToString on the string in the .increment calls

@unity-cla-assistant
Copy link

unity-cla-assistant commented Feb 26, 2021

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@mattwalsh-unity mattwalsh-unity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I went ahead and removed the extra toString calls

@mattwalsh-unity mattwalsh-unity merged commit 18c1a0a into develop Feb 26, 2021
@0xFA11 0xFA11 deleted the feature/performance_data_propagation branch March 5, 2021 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants