using System.Collections.Generic;
using System.Diagnostics;
using Unity.Barracuda;
using Unity.MLAgents.Actuators;
using Unity.MLAgents.Inference;
using Unity.MLAgents.Policies;
using Unity.MLAgents.Sensors;
using UnityEngine;
#if MLA_UNITY_ANALYTICS_MODULE
using UnityEngine.Analytics;
#endif
#if UNITY_EDITOR
using UnityEditor;
#if MLA_UNITY_ANALYTICS_MODULE
using UnityEditor.Analytics;
#endif // MLA_UNITY_ANALYTICS_MODULE
#endif // UNITY_EDITOR
namespace Unity.MLAgents.Analytics
{
internal class InferenceAnalytics
{
const string k_VendorKey = "unity.ml-agents";
const string k_EventName = "ml_agents_inferencemodelset";
const int k_EventVersion = 1;
///
/// Whether or not we've registered this particular event yet
///
static bool s_EventRegistered;
///
/// Hourly limit for this event name
///
const int k_MaxEventsPerHour = 1000;
///
/// Maximum number of items in this event.
///
const int k_MaxNumberOfElements = 1000;
#if UNITY_EDITOR && MLA_UNITY_ANALYTICS_MODULE
///
/// Models that we've already sent events for.
///
private static HashSet s_SentModels;
#endif
static bool EnableAnalytics()
{
#if UNITY_EDITOR && MLA_UNITY_ANALYTICS_MODULE
if (s_EventRegistered)
{
return true;
}
AnalyticsResult result = EditorAnalytics.RegisterEventWithLimit(k_EventName, k_MaxEventsPerHour, k_MaxNumberOfElements, k_VendorKey, k_EventVersion);
if (result == AnalyticsResult.Ok)
{
s_EventRegistered = true;
}
if (s_EventRegistered && s_SentModels == null)
{
s_SentModels = new HashSet();
}
#else // no editor, no analytics
s_EventRegistered = false;
#endif
return s_EventRegistered;
}
public static bool IsAnalyticsEnabled()
{
#if UNITY_EDITOR
return EditorAnalytics.enabled;
#else
return false;
#endif
}
///
/// Send an analytics event for the NNModel when it is set up for inference.
/// No events will be sent if analytics are disabled, and at most one event
/// will be sent per model instance.
///
/// The NNModel being used for inference.
/// The BehaviorName of the Agent using the model
/// Whether inference is being performed on the CPU or GPU
/// List of ISensors for the Agent. Used to generate information about the observation space.
/// ActionSpec for the Agent. Used to generate information about the action space.
/// List of IActuators for the Agent. Used to generate information about the action space.
///
[Conditional("MLA_UNITY_ANALYTICS_MODULE")]
public static void InferenceModelSet(
NNModel nnModel,
string behaviorName,
InferenceDevice inferenceDevice,
IList sensors,
ActionSpec actionSpec,
IList actuators
)
{
#if UNITY_EDITOR && MLA_UNITY_ANALYTICS_MODULE
// The event shouldn't be able to report if this is disabled but if we know we're not going to report
// Lets early out and not waste time gathering all the data
if (!IsAnalyticsEnabled())
return;
if (!EnableAnalytics())
return;
var added = s_SentModels.Add(nnModel);
if (!added)
{
// We previously added this model. Exit so we don't resend.
return;
}
var data = GetEventForModel(nnModel, behaviorName, inferenceDevice, sensors, actionSpec, actuators);
// Note - to debug, use JsonUtility.ToJson on the event.
// Debug.Log(JsonUtility.ToJson(data, true));
if (AnalyticsUtils.s_SendEditorAnalytics)
{
EditorAnalytics.SendEventWithLimit(k_EventName, data, k_EventVersion);
}
#endif
}
///
/// Generate an InferenceEvent for the model.
///
///
///
///
///
///
///
///
internal static InferenceEvent GetEventForModel(
NNModel nnModel,
string behaviorName,
InferenceDevice inferenceDevice,
IList sensors,
ActionSpec actionSpec,
IList actuators
)
{
var barracudaModel = ModelLoader.Load(nnModel);
var inferenceEvent = new InferenceEvent();
// Hash the behavior name so that there's no concern about PII or "secret" data being leaked.
inferenceEvent.BehaviorName = AnalyticsUtils.Hash(behaviorName);
inferenceEvent.BarracudaModelSource = barracudaModel.IrSource;
inferenceEvent.BarracudaModelVersion = barracudaModel.IrVersion;
inferenceEvent.BarracudaModelProducer = barracudaModel.ProducerName;
inferenceEvent.MemorySize = (int)barracudaModel.GetTensorByName(TensorNames.MemorySize)[0];
inferenceEvent.InferenceDevice = (int)inferenceDevice;
if (barracudaModel.ProducerName == "Script")
{
// .nn files don't have these fields set correctly. Assign some placeholder values.
inferenceEvent.BarracudaModelSource = "NN";
inferenceEvent.BarracudaModelProducer = "tensorflow_to_barracuda.py";
}
#if UNITY_EDITOR
var barracudaPackageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(Tensor).Assembly);
inferenceEvent.BarracudaPackageVersion = barracudaPackageInfo.version;
#else
inferenceEvent.BarracudaPackageVersion = null;
#endif
inferenceEvent.ActionSpec = EventActionSpec.FromActionSpec(actionSpec);
inferenceEvent.ObservationSpecs = new List(sensors.Count);
foreach (var sensor in sensors)
{
inferenceEvent.ObservationSpecs.Add(EventObservationSpec.FromSensor(sensor));
}
inferenceEvent.ActuatorInfos = new List(actuators.Count);
foreach (var actuator in actuators)
{
inferenceEvent.ActuatorInfos.Add(EventActuatorInfo.FromActuator(actuator));
}
inferenceEvent.TotalWeightSizeBytes = GetModelWeightSize(barracudaModel);
inferenceEvent.ModelHash = GetModelHash(barracudaModel);
return inferenceEvent;
}
///
/// Compute the total model weight size in bytes.
/// This corresponds to the "Total weight size" display in the Barracuda inspector,
/// and the calculations are the same.
///
///
///
static long GetModelWeightSize(Model barracudaModel)
{
long totalWeightsSizeInBytes = 0;
for (var l = 0; l < barracudaModel.layers.Count; ++l)
{
for (var d = 0; d < barracudaModel.layers[l].datasets.Length; ++d)
{
totalWeightsSizeInBytes += barracudaModel.layers[l].datasets[d].length;
}
}
return totalWeightsSizeInBytes;
}
///
/// Wrapper around Hash128 that supports Append(float[], int, int)
///
struct MLAgentsHash128
{
private Hash128 m_Hash;
public void Append(float[] values, int count)
{
if (values == null)
{
return;
}
// Pre-2020 versions of Unity don't have Hash128.Append() (can only hash strings and scalars)
// For these versions, we'll hash element by element.
#if UNITY_2020_1_OR_NEWER
m_Hash.Append(values, 0, count);
#else
for (var i = 0; i < count; i++)
{
var tempHash = new Hash128();
HashUtilities.ComputeHash128(ref values[i], ref tempHash);
HashUtilities.AppendHash(ref tempHash, ref m_Hash);
}
#endif
}
public void Append(string value)
{
var tempHash = Hash128.Compute(value);
HashUtilities.AppendHash(ref tempHash, ref m_Hash);
}
public override string ToString()
{
return m_Hash.ToString();
}
}
///
/// Compute a hash of the model's layer data and return it as a string.
/// A subset of the layer weights are used for performance.
/// This increases the chance of a collision, but this should still be extremely rare.
///
///
///
static string GetModelHash(Model barracudaModel)
{
var hash = new MLAgentsHash128();
// Limit the max number of float bytes that we hash for performance.
const int kMaxFloats = 256;
foreach (var layer in barracudaModel.layers)
{
hash.Append(layer.name);
var numFloatsToHash = Mathf.Min(layer.weights.Length, kMaxFloats);
hash.Append(layer.weights, numFloatsToHash);
}
return hash.ToString();
}
}
}