Skip to content

Commit d4b6c01

Browse files
authored
Export Unity profiler statistics as Streamline counters (#4)
This PR adds support for exporting Unity profiler statistics as Streamline counters. * Bump package version to 1.2.0. * Increases minimum Unity version to 2020.3 (latest LTS).
1 parent a25744e commit d4b6c01

4 files changed

Lines changed: 254 additions & 9 deletions

File tree

Runtime/MobileStudio.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* SPDX-License-Identifier: BSD-3-Clause
33
*
4-
* Copyright (c) 2021, Arm Limited
4+
* Copyright (c) 2021-2022, Arm Limited
55
* All rights reserved.
66
*
77
* Redistribution and use in source and binary forms, with or without
@@ -52,6 +52,8 @@ public enum CounterType { Absolute, Delta };
5252

5353
private static AnnotationState state = getAnnotationState();
5454

55+
internal static bool Active => state == AnnotationState.Active;
56+
5557
#if UNITY_ANDROID && !UNITY_EDITOR
5658
[DllImport("mobilestudio")]
5759
private static extern void gator_annotate_setup();
@@ -229,7 +231,7 @@ public class Counter
229231
/*
230232
* Specify the counter chart title, series name, and value type.
231233
*/
232-
public Counter(string title, string name, CounterType type)
234+
public Counter(string title, string name, CounterType type, string unit = null)
233235
{
234236
lock(_locker)
235237
{
@@ -245,7 +247,7 @@ public Counter(string title, string name, CounterType type)
245247

246248
gator_annotate_counter(
247249
counter, title, name, 0, counterClass, displayClass,
248-
null, modifier, RC_OVERLAY, RC_LINE, 0, 0, 0,
250+
unit, modifier, RC_OVERLAY, RC_LINE, 0, 0, 0,
249251
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 0, 0, null);
250252
}
251253
#endif

Runtime/UnityStatsProxy.cs

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* SPDX-License-Identifier: BSD-3-Clause
3+
*
4+
* Copyright (c) 2022, Arm Limited
5+
* All rights reserved.
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions
9+
* are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright
12+
* notice, this list of conditions and the following disclaimer.
13+
*
14+
* 2. Redistributions in binary form must reproduce the above copyright
15+
* notice, this list of conditions and the following disclaimer in the
16+
* documentation and/or other materials provided with the distribution.
17+
*
18+
* 3. Neither the name of the copyright holder nor the names of its
19+
* contributors may be used to endorse or promote products derived from
20+
* this software without specific prior written permission.
21+
*
22+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
25+
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
28+
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
// Only active in Android Players
36+
#if UNITY_ANDROID && !UNITY_EDITOR
37+
38+
using System.Collections.Generic;
39+
using System.Runtime.InteropServices;
40+
using Unity.Profiling;
41+
using UnityEngine;
42+
using UnityEngine.LowLevel;
43+
using UnityEngine.Scripting;
44+
45+
[assembly: AlwaysLinkAssembly]
46+
47+
namespace MobileStudio
48+
{
49+
class UnityStatsProxy
50+
{
51+
static UnityStatsProxy Instance { get; set; }
52+
53+
struct CounterObjectPair
54+
{
55+
public CounterObjectPair(Annotations.Counter mobileStudioCounter, ProfilerRecorder unityCounter, bool convertToMiB)
56+
{
57+
this.mobileStudioCounter = mobileStudioCounter;
58+
this.unityCounter = unityCounter;
59+
this.convertToMiB = convertToMiB;
60+
}
61+
62+
public Annotations.Counter mobileStudioCounter;
63+
public ProfilerRecorder unityCounter;
64+
public bool convertToMiB;
65+
}
66+
List<CounterObjectPair> counters;
67+
68+
struct CounterMapping
69+
{
70+
public CounterMapping(string mobileStudioTitle, string mobileStudioCounterName, string mobileStudioCounterUnit, string unityCounterName)
71+
{
72+
this.mobileStudioTitle = mobileStudioTitle;
73+
this.mobileStudioCounterName = mobileStudioCounterName;
74+
this.mobileStudioCounterUnit = mobileStudioCounterUnit;
75+
this.unityCounterName = unityCounterName;
76+
}
77+
78+
public string mobileStudioTitle;
79+
public string mobileStudioCounterName;
80+
public string mobileStudioCounterUnit;
81+
public string unityCounterName;
82+
}
83+
84+
// Memory counters are captured based on https://docs.unity3d.com/Manual/ProfilerMemory.html information.
85+
static readonly string memoryStatsTitle = "Unity Memory Usage";
86+
static readonly string objectStatsTitle = "Unity Objects";
87+
static readonly string unitBytes = "MiB";
88+
static readonly string unitObjects = "objects";
89+
const double toMiB = 1.0 / (1024.0 * 1024.0);
90+
static readonly CounterMapping[] mobileStudioDefaultCounters =
91+
{
92+
new CounterMapping(memoryStatsTitle, "Total Memory Usage", unitBytes, "Total Reserved Memory"),
93+
new CounterMapping(memoryStatsTitle, "Scripting Memory Usage", unitBytes, "GC Reserved Memory"),
94+
// Development player memory stats
95+
new CounterMapping(memoryStatsTitle, "Graphics Memory Usage", unitBytes, "Gfx Reserved Memory"),
96+
// Development player object stats
97+
new CounterMapping(objectStatsTitle, "Object Count", unitObjects, "Object Count"),
98+
new CounterMapping(objectStatsTitle, "Game Object Count", unitObjects, "Game Object Count"),
99+
new CounterMapping(objectStatsTitle, "Texture Count", unitObjects, "Texture Count"),
100+
new CounterMapping(objectStatsTitle, "Mesh Count", unitObjects, "Mesh Count"),
101+
new CounterMapping(objectStatsTitle, "Material Count", unitObjects, "Material Count"),
102+
new CounterMapping(objectStatsTitle, "AnimationClip Count", unitObjects, "AnimationClip Count"),
103+
};
104+
105+
[RuntimeInitializeOnLoadMethod]
106+
[Preserve]
107+
static void InitializeUnityStatsProxy()
108+
{
109+
if (Instance == null && Annotations.Active)
110+
{
111+
Instance = new UnityStatsProxy();
112+
}
113+
}
114+
115+
UnityStatsProxy()
116+
{
117+
if (!InstallUpdateCallback())
118+
{
119+
UnityEngine.Debug.LogError("UnityStatsProxy: Failed to inject Playerloop callback!");
120+
return;
121+
}
122+
123+
InitializeCounters();
124+
}
125+
126+
~UnityStatsProxy()
127+
{
128+
// Cleanup Unity counters
129+
DisposeCounters();
130+
}
131+
132+
bool InstallUpdateCallback()
133+
{
134+
var root = PlayerLoop.GetCurrentPlayerLoop();
135+
var injectionPoint = typeof(UnityEngine.PlayerLoop.PreLateUpdate);
136+
var result = InjectPlayerLoopCallback(ref root, injectionPoint, Update);
137+
PlayerLoop.SetPlayerLoop(root);
138+
return result;
139+
}
140+
141+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
142+
delegate uint PlayerLoopDelegate();
143+
144+
static bool InjectPlayerLoopCallback(ref PlayerLoopSystem system, System.Type injectedSystem, PlayerLoopSystem.UpdateFunction injectedFnc)
145+
{
146+
// Have we found the system we're looking for?
147+
if (system.type == injectedSystem)
148+
{
149+
// If system has updateFunction set, updateDelegate won't be called
150+
// We wrap native update function in something we can call in c#
151+
// Reset updateFunction and call wrapped updateFunction in our delegate
152+
PlayerLoopDelegate systemDelegate = null;
153+
if (system.updateFunction.ToInt64() != 0)
154+
{
155+
var intPtr = Marshal.ReadIntPtr(system.updateFunction);
156+
if (intPtr.ToInt64() != 0)
157+
{
158+
systemDelegate = (PlayerLoopDelegate)Marshal.GetDelegateForFunctionPointer(intPtr, typeof(PlayerLoopDelegate));
159+
}
160+
}
161+
162+
// Install the new delegate and keep the system function call
163+
system.updateDelegate = () => { injectedFnc(); if (systemDelegate != null) _ = systemDelegate(); };
164+
system.updateFunction = new System.IntPtr(0);
165+
166+
return true;
167+
}
168+
169+
if (system.subSystemList == null)
170+
{
171+
return false;
172+
}
173+
174+
// Iterate all subsystems
175+
for (int i = 0; i < system.subSystemList.Length; ++i)
176+
{
177+
if (InjectPlayerLoopCallback(ref system.subSystemList[i], injectedSystem, injectedFnc))
178+
{
179+
return true;
180+
}
181+
}
182+
183+
return false;
184+
}
185+
186+
void InitializeCounters()
187+
{
188+
// Map Unity counters to Mobile Studio
189+
counters = new List<CounterObjectPair>();
190+
for (var i = 0; i < mobileStudioDefaultCounters.Length; ++i)
191+
{
192+
var recorder = new ProfilerRecorder(mobileStudioDefaultCounters[i].unityCounterName, 0);
193+
if (!recorder.Valid)
194+
{
195+
recorder.Dispose();
196+
continue;
197+
}
198+
199+
var mobileStudioCounter = new Annotations.Counter(mobileStudioDefaultCounters[i].mobileStudioTitle, mobileStudioDefaultCounters[i].mobileStudioCounterName, Annotations.CounterType.Absolute, mobileStudioDefaultCounters[i].mobileStudioCounterUnit);
200+
counters.Add(new CounterObjectPair(mobileStudioCounter, recorder, mobileStudioDefaultCounters[i].mobileStudioCounterUnit == unitBytes));
201+
}
202+
}
203+
204+
void DisposeCounters()
205+
{
206+
if (counters == null)
207+
{
208+
return;
209+
}
210+
211+
for (var i = 0; i < counters.Count; ++i)
212+
{
213+
counters[i].unityCounter.Dispose();
214+
}
215+
216+
counters = null;
217+
}
218+
219+
void Update()
220+
{
221+
// Report Unity counters to MobileStudio on a frame basis
222+
for (var i = 0; i < counters.Count; ++i)
223+
{
224+
var value = counters[i].unityCounter.CurrentValueAsDouble;
225+
float valueToReport = counters[i].convertToMiB ? (float)(value * toMiB) : (float)value;
226+
counters[i].mobileStudioCounter.set_value(valueToReport);
227+
}
228+
}
229+
}
230+
}
231+
232+
#endif

Runtime/UnityStatsProxy.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "com.arm.mobile-studio",
3-
"description": "Unity C# bindings for Mobile Studio.",
4-
"version": "1.0.0",
5-
"unity": "2018.4",
6-
"displayName": "Mobile Studio"
7-
}
2+
"name": "com.arm.mobile-studio",
3+
"description": "Unity C# bindings for Mobile Studio.",
4+
"version": "1.2.0",
5+
"unity": "2020.3",
6+
"displayName": "Mobile Studio"
7+
}

0 commit comments

Comments
 (0)