@@ -1,242 +0,0 @@ | |||
#if UNITY_EDITOR && ENGINE_PROFILER_ENABLED | |||
using System; | |||
using UnityEditor; | |||
using UnityEngine; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
[CustomEditor(typeof (EngineProfilerBehaviour))] | |||
public class EngineProfilerInspector : Editor | |||
{ | |||
enum SORTING_OPTIONS | |||
{ | |||
AVERAGE, | |||
MIN, | |||
MAX, | |||
NAME, | |||
NONE | |||
} | |||
static bool _hideEmptyEngines = true; | |||
static bool _showAddEngines; | |||
static bool _showRemoveEngines; | |||
static string _systemNameSearchTerm = string.Empty; | |||
string minTitle = "Min".PadRight(15, ' '); | |||
string maxTitle = "Max".PadRight(15, ' '); | |||
string avgTitle = "Avg".PadRight(15, ' '); | |||
SORTING_OPTIONS _sortingOption = SORTING_OPTIONS.AVERAGE; | |||
public override void OnInspectorGUI() | |||
{ | |||
var engineProfilerBehaviour = (EngineProfilerBehaviour) target; | |||
EngineInfo[] engines = new EngineInfo[engineProfilerBehaviour.engines.Count]; | |||
engineProfilerBehaviour.engines.CopyTo(engines, 0); | |||
DrawEngineList(engineProfilerBehaviour, engines); | |||
EditorUtility.SetDirty(target); | |||
} | |||
void DrawEngineList(EngineProfilerBehaviour engineProfilerBehaviour, EngineInfo[] engines) | |||
{ | |||
ProfilerEditorLayout.BeginVerticalBox(); | |||
{ | |||
ProfilerEditorLayout.BeginHorizontal(); | |||
{ | |||
if (GUILayout.Button("Reset Durations", GUILayout.Width(120), GUILayout.Height(14))) | |||
{ | |||
engineProfilerBehaviour.ResetDurations(); | |||
} | |||
} | |||
ProfilerEditorLayout.EndHorizontal(); | |||
_sortingOption = (SORTING_OPTIONS) EditorGUILayout.EnumPopup("Sort By:", _sortingOption); | |||
_hideEmptyEngines = EditorGUILayout.Toggle("Hide empty systems", _hideEmptyEngines); | |||
EditorGUILayout.Space(); | |||
ProfilerEditorLayout.BeginHorizontal(); | |||
{ | |||
_systemNameSearchTerm = EditorGUILayout.TextField("Search", _systemNameSearchTerm); | |||
const string clearButtonControlName = "Clear Button"; | |||
GUI.SetNextControlName(clearButtonControlName); | |||
if (GUILayout.Button("x", GUILayout.Width(19), GUILayout.Height(14))) | |||
{ | |||
_systemNameSearchTerm = string.Empty; | |||
GUI.FocusControl(clearButtonControlName); | |||
} | |||
} | |||
ProfilerEditorLayout.EndHorizontal(); | |||
_showAddEngines = EditorGUILayout.Foldout(_showAddEngines, "Engines Add"); | |||
if (_showAddEngines && ShouldShowSystems(engines)) | |||
{ | |||
ProfilerEditorLayout.BeginVerticalBox(); | |||
{ | |||
var systemsDrawn = DrawAddEngineInfos(engines); | |||
if (systemsDrawn == 0) | |||
{ | |||
EditorGUILayout.LabelField(string.Empty); | |||
} | |||
} | |||
ProfilerEditorLayout.EndVertical(); | |||
} | |||
_showRemoveEngines = EditorGUILayout.Foldout(_showRemoveEngines, "Engines Remove"); | |||
if (_showRemoveEngines && ShouldShowSystems(engines)) | |||
{ | |||
ProfilerEditorLayout.BeginVerticalBox(); | |||
{ | |||
var systemsDrawn = DrawRemoveEngineInfos(engines); | |||
if (systemsDrawn == 0) | |||
{ | |||
EditorGUILayout.LabelField(string.Empty); | |||
} | |||
} | |||
ProfilerEditorLayout.EndVertical(); | |||
} | |||
} | |||
ProfilerEditorLayout.EndVertical(); | |||
} | |||
int DrawAddEngineInfos(EngineInfo[] engines) | |||
{ | |||
if (_sortingOption != SORTING_OPTIONS.NONE) | |||
{ | |||
SortAddEngines(engines); | |||
} | |||
string title = avgTitle.FastConcat(minTitle).FastConcat(maxTitle); | |||
EditorGUILayout.LabelField("Engine Name", title, EditorStyles.boldLabel); | |||
int enginesDrawn = 0; | |||
for (int i = 0; i < engines.Length; i++) | |||
{ | |||
EngineInfo engineInfo = engines[i]; | |||
if (engineInfo.engineName.ToLower().Contains(_systemNameSearchTerm.ToLower()) && | |||
!engineInfo.minAddDuration.Equals(0) && !engineInfo.maxAddDuration.Equals(0)) | |||
{ | |||
ProfilerEditorLayout.BeginHorizontal(); | |||
{ | |||
var avg = string.Format("{0:0.000}", engineInfo.averageAddDuration).PadRight(15); | |||
var min = string.Format("{0:0.000}", engineInfo.minAddDuration).PadRight(15); | |||
var max = string.Format("{0:0.000}", engineInfo.maxAddDuration); | |||
string output = avg.FastConcat(min).FastConcat(max); | |||
EditorGUILayout.LabelField(engineInfo.engineName, output, GetEngineStyle()); | |||
} | |||
ProfilerEditorLayout.EndHorizontal(); | |||
enginesDrawn += 1; | |||
} | |||
} | |||
return enginesDrawn; | |||
} | |||
int DrawRemoveEngineInfos(EngineInfo[] engines) | |||
{ | |||
if (_sortingOption != SORTING_OPTIONS.NONE) | |||
{ | |||
SortRemoveEngines(engines); | |||
} | |||
string title = avgTitle.FastConcat(minTitle).FastConcat(maxTitle); | |||
EditorGUILayout.LabelField("Engine Name", title, EditorStyles.boldLabel); | |||
int enginesDrawn = 0; | |||
for (int i = 0; i < engines.Length; i++) | |||
{ | |||
EngineInfo engineInfo = engines[i]; | |||
if (engineInfo.engineName.ToLower().Contains(_systemNameSearchTerm.ToLower()) && | |||
!engineInfo.minRemoveDuration.Equals(0) && !engineInfo.maxRemoveDuration.Equals(0)) | |||
{ | |||
ProfilerEditorLayout.BeginHorizontal(); | |||
{ | |||
var avg = string.Format("{0:0.000}", engineInfo.averageRemoveDuration).PadRight(15); | |||
var min = string.Format("{0:0.000}", engineInfo.minRemoveDuration).PadRight(15); | |||
var max = string.Format("{0:0.000}", engineInfo.maxRemoveDuration); | |||
string output = avg.FastConcat(min).FastConcat(max); | |||
EditorGUILayout.LabelField(engineInfo.engineName, output, GetEngineStyle()); | |||
} | |||
ProfilerEditorLayout.EndHorizontal(); | |||
enginesDrawn += 1; | |||
} | |||
} | |||
return enginesDrawn; | |||
} | |||
static GUIStyle GetEngineStyle() | |||
{ | |||
var style = new GUIStyle(GUI.skin.label); | |||
var color = EditorGUIUtility.isProSkin ? Color.white : style.normal.textColor; | |||
style.normal.textColor = color; | |||
return style; | |||
} | |||
static bool ShouldShowSystems(EngineInfo[] engines) | |||
{ | |||
return engines.Length > 0; | |||
} | |||
#region Sorting Engines | |||
void SortAddEngines(EngineInfo[] engines) | |||
{ | |||
switch (_sortingOption) | |||
{ | |||
case SORTING_OPTIONS.AVERAGE: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.averageAddDuration.CompareTo(engine1.averageAddDuration)); | |||
break; | |||
case SORTING_OPTIONS.MIN: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.minAddDuration.CompareTo(engine1.minAddDuration)); | |||
break; | |||
case SORTING_OPTIONS.MAX: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.maxAddDuration.CompareTo(engine1.maxAddDuration)); | |||
break; | |||
case SORTING_OPTIONS.NAME: | |||
Array.Sort(engines, | |||
(engine1, engine2) => String.Compare(engine1.engineName, engine2.engineName, StringComparison.Ordinal)); | |||
break; | |||
} | |||
} | |||
void SortRemoveEngines(EngineInfo[] engines) | |||
{ | |||
switch (_sortingOption) | |||
{ | |||
case SORTING_OPTIONS.AVERAGE: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.averageRemoveDuration.CompareTo(engine1.averageRemoveDuration)); | |||
break; | |||
case SORTING_OPTIONS.MIN: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.minRemoveDuration.CompareTo(engine1.minRemoveDuration)); | |||
break; | |||
case SORTING_OPTIONS.MAX: | |||
Array.Sort(engines, | |||
(engine1, engine2) => engine2.maxRemoveDuration.CompareTo(engine1.maxRemoveDuration)); | |||
break; | |||
case SORTING_OPTIONS.NAME: | |||
Array.Sort(engines, | |||
(engine1, engine2) => String.Compare(engine1.engineName, engine2.engineName, StringComparison.Ordinal)); | |||
break; | |||
} | |||
} | |||
} | |||
#endregion | |||
} | |||
#endif |
@@ -1,24 +0,0 @@ | |||
#if UNITY_EDITOR | |||
using UnityEditor; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
class EngineProfilerMenuItem | |||
{ | |||
[MenuItem("Engines/Enable Profiler")] | |||
public static void EnableProfiler() | |||
{ | |||
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, "ENGINE_PROFILER_ENABLED"); | |||
} | |||
[MenuItem("Engines/Disable Profiler")] | |||
public static void DisableProfiler() | |||
{ | |||
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, ""); | |||
} | |||
} | |||
} | |||
#endif |
@@ -1,134 +0,0 @@ | |||
#if UNITY_EDITOR && ENGINE_PROFILER_ENABLED | |||
using System.Linq; | |||
using UnityEditor; | |||
using UnityEngine; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
public class EnginesMonitor | |||
{ | |||
public float xBorder = 48; | |||
public float yBorder = 20; | |||
public int rightLinePadding = -15; | |||
public string labelFormat = "{0:0.0}"; | |||
public string axisFormat = "{0:0.0}"; | |||
public int gridLines = 1; | |||
public float axisRounding = 5f; | |||
public float anchorRadius = 1f; | |||
public Color lineColor = Color.magenta; | |||
readonly GUIStyle _labelTextStyle; | |||
readonly GUIStyle _centeredStyle; | |||
readonly Vector3[] _cachedLinePointVerticies; | |||
readonly Vector3[] _linePoints; | |||
public EnginesMonitor(int dataLength) | |||
{ | |||
_labelTextStyle = new GUIStyle(GUI.skin.label); | |||
_labelTextStyle.alignment = TextAnchor.UpperRight; | |||
_centeredStyle = new GUIStyle(); | |||
_centeredStyle.alignment = TextAnchor.UpperCenter; | |||
_centeredStyle.normal.textColor = Color.white; | |||
_linePoints = new Vector3[dataLength]; | |||
_cachedLinePointVerticies = new[] | |||
{ | |||
new Vector3(-1, 1, 0)*anchorRadius, | |||
new Vector3(1, 1, 0)*anchorRadius, | |||
new Vector3(1, -1, 0)*anchorRadius, | |||
new Vector3(-1, -1, 0)*anchorRadius, | |||
}; | |||
} | |||
public void Draw(float[] data, float height, float axisUpperBounds) | |||
{ | |||
axisRounding = axisUpperBounds; | |||
var rect = GUILayoutUtility.GetRect(EditorGUILayout.GetControlRect().width, height); | |||
var top = rect.y + yBorder; | |||
var floor = rect.y + rect.height - yBorder; | |||
var availableHeight = floor - top; | |||
var max = data.Length != 0 ? data.Max() : 0f; | |||
if (max%axisRounding != 0) | |||
{ | |||
max = max + axisRounding - (max%axisRounding); | |||
} | |||
drawGridLines(top, rect.width, availableHeight, max); | |||
drawLine(data, floor, rect.width, availableHeight, max); | |||
} | |||
void drawGridLines(float top, float width, float availableHeight, float max) | |||
{ | |||
var handleColor = Handles.color; | |||
Handles.color = Color.grey; | |||
var n = gridLines + 1; | |||
var lineSpacing = availableHeight/n; | |||
for (int i = 0; i <= n; i++) | |||
{ | |||
var lineY = top + (lineSpacing*i); | |||
Handles.DrawLine( | |||
new Vector2(xBorder, lineY), | |||
new Vector2(width - rightLinePadding, lineY) | |||
); | |||
GUI.Label( | |||
new Rect(0, lineY - 8, xBorder - 2, 50), | |||
string.Format(axisFormat, max*(1f - ((float) i/(float) n))), | |||
_labelTextStyle | |||
); | |||
} | |||
Handles.color = handleColor; | |||
} | |||
void drawLine(float[] data, float floor, float width, float availableHeight, float max) | |||
{ | |||
var lineWidth = (float) (width - xBorder - rightLinePadding)/data.Length; | |||
var handleColor = Handles.color; | |||
var labelRect = new Rect(); | |||
Vector2 newLine; | |||
bool mousePositionDiscovered = false; | |||
float mouseHoverDataValue = 0; | |||
float linePointScale; | |||
Handles.color = lineColor; | |||
Handles.matrix = Matrix4x4.identity; | |||
HandleUtility.handleMaterial.SetPass(0); | |||
for (int i = 0; i < data.Length; i++) | |||
{ | |||
var value = data[i]; | |||
var lineTop = floor - (availableHeight*(value/max)); | |||
newLine = new Vector2(xBorder + (lineWidth*i), lineTop); | |||
_linePoints[i] = new Vector3(newLine.x, newLine.y, 0); | |||
linePointScale = 1f; | |||
if (!mousePositionDiscovered) | |||
{ | |||
var anchorPosRadius3 = anchorRadius*3; | |||
var anchorPosRadius6 = anchorRadius*6; | |||
var anchorPos = newLine - (Vector2.up*0.5f); | |||
labelRect = new Rect(anchorPos.x - anchorPosRadius3, anchorPos.y - anchorPosRadius3, | |||
anchorPosRadius6, anchorPosRadius6); | |||
if (labelRect.Contains(Event.current.mousePosition)) | |||
{ | |||
mousePositionDiscovered = true; | |||
mouseHoverDataValue = value; | |||
linePointScale = 3f; | |||
} | |||
} | |||
Handles.matrix = Matrix4x4.TRS(_linePoints[i], Quaternion.identity, Vector3.one*linePointScale); | |||
Handles.DrawAAConvexPolygon(_cachedLinePointVerticies); | |||
} | |||
Handles.matrix = Matrix4x4.identity; | |||
Handles.DrawAAPolyLine(2f, data.Length, _linePoints); | |||
if (mousePositionDiscovered) | |||
{ | |||
labelRect.y -= 16; | |||
labelRect.width += 50; | |||
labelRect.x -= 25; | |||
GUI.Label(labelRect, string.Format(labelFormat, mouseHoverDataValue), _centeredStyle); | |||
} | |||
Handles.color = handleColor; | |||
} | |||
} | |||
} | |||
#endif |
@@ -1,67 +0,0 @@ | |||
#if UNITY_EDITOR && ENGINE_PROFILER_ENABLED | |||
using UnityEditor; | |||
using UnityEngine; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
public static class ProfilerEditorLayout | |||
{ | |||
public static void ShowWindow<T>(string title) where T : EditorWindow | |||
{ | |||
var window = EditorWindow.GetWindow<T>(true, title); | |||
window.minSize = window.maxSize = new Vector2(415f, 520f); | |||
window.Show(); | |||
} | |||
public static Texture2D LoadTexture(string label) | |||
{ | |||
var guid = AssetDatabase.FindAssets(label)[0]; | |||
if (guid != null) | |||
{ | |||
var path = AssetDatabase.GUIDToAssetPath(guid); | |||
return AssetDatabase.LoadAssetAtPath<Texture2D>(path); | |||
} | |||
return null; | |||
} | |||
public static float DrawHeaderTexture(EditorWindow window, Texture2D texture) | |||
{ | |||
const int scollBarWidth = 15; | |||
var ratio = texture.width/texture.height; | |||
var width = window.position.width - 8 - scollBarWidth; | |||
var height = width/ratio; | |||
GUI.DrawTexture(new Rect(4, 2, width, height), texture, ScaleMode.ScaleToFit); | |||
return height; | |||
} | |||
public static Rect BeginVertical() | |||
{ | |||
return EditorGUILayout.BeginVertical(); | |||
} | |||
public static Rect BeginVerticalBox(GUIStyle style = null) | |||
{ | |||
return EditorGUILayout.BeginVertical(style ?? GUI.skin.box); | |||
} | |||
public static void EndVertical() | |||
{ | |||
EditorGUILayout.EndVertical(); | |||
} | |||
public static Rect BeginHorizontal() | |||
{ | |||
return EditorGUILayout.BeginHorizontal(); | |||
} | |||
public static void EndHorizontal() | |||
{ | |||
EditorGUILayout.EndHorizontal(); | |||
} | |||
} | |||
} | |||
#endif |
@@ -1,87 +0,0 @@ | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
public sealed class EngineInfo | |||
{ | |||
readonly IEngine _engine; | |||
readonly string _engineName; | |||
const int NUM_UPDATE_TYPES = 3; | |||
const int NUM_FRAMES_TO_AVERAGE = 10; | |||
double _accumulatedAddDuration; | |||
double _minAddDuration; | |||
double _maxAddDuration; | |||
int _entityViewsAddedCount; | |||
double _accumulatedRemoveDuration; | |||
double _minRemoveDuration; | |||
double _maxRemoveDuration; | |||
int _entityViewsRemovedCount; | |||
public string engineName { get { return _engineName; } } | |||
public double minAddDuration { get { return _minAddDuration; } } | |||
public double minRemoveDuration { get { return _minRemoveDuration; } } | |||
public double maxAddDuration { get { return _maxAddDuration; } } | |||
public double maxRemoveDuration { get { return _maxRemoveDuration; } } | |||
public double averageAddDuration { get { return _entityViewsAddedCount == 0 ? 0 : _accumulatedAddDuration / _entityViewsAddedCount; } } | |||
public double averageRemoveDuration { get { return _entityViewsRemovedCount == 0 ? 0 : _accumulatedRemoveDuration / _entityViewsRemovedCount; } } | |||
public EngineInfo(IEngine engine) | |||
{ | |||
_engine = engine; | |||
_engineName = _engine.ToString(); | |||
int foundNamespace = _engineName.LastIndexOf("."); | |||
_engineName = _engineName.Remove(0, foundNamespace + 1); | |||
ResetDurations(); | |||
} | |||
public void AddAddDuration(double duration) | |||
{ | |||
if (duration < _minAddDuration || _minAddDuration == 0) | |||
{ | |||
_minAddDuration = duration; | |||
} | |||
if (duration > _maxAddDuration) | |||
{ | |||
_maxAddDuration = duration; | |||
} | |||
_accumulatedAddDuration += duration; | |||
_entityViewsAddedCount += 1; | |||
} | |||
public void AddRemoveDuration(double duration) | |||
{ | |||
if (duration < _minRemoveDuration || _minRemoveDuration == 0) | |||
{ | |||
_minRemoveDuration = duration; | |||
} | |||
if (duration > _maxRemoveDuration) | |||
{ | |||
_maxRemoveDuration = duration; | |||
} | |||
_accumulatedRemoveDuration += duration; | |||
_entityViewsRemovedCount += 1; | |||
} | |||
public void ResetDurations() | |||
{ | |||
_accumulatedAddDuration = 0; | |||
_minAddDuration = 0; | |||
_maxAddDuration = 0; | |||
_entityViewsAddedCount = 0; | |||
_accumulatedRemoveDuration = 0; | |||
_minRemoveDuration = 0; | |||
_maxRemoveDuration = 0; | |||
_entityViewsRemovedCount = 0; | |||
} | |||
} | |||
} |
@@ -1,60 +0,0 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using Svelto.ECS.Internal; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
public sealed class EngineProfiler | |||
{ | |||
static readonly Stopwatch _stopwatch = new Stopwatch(); | |||
public static void MonitorAddDuration<T>(IHandleEntityViewEngineAbstracted engine, ref T entityView) | |||
{ | |||
EngineInfo info; | |||
if (engineInfos.TryGetValue(engine.GetType(), out info)) | |||
{ | |||
_stopwatch.Start(); | |||
(engine as IHandleEntityStructEngine<T>).AddInternal(ref entityView); | |||
_stopwatch.Stop(); | |||
info.AddAddDuration(_stopwatch.Elapsed.TotalMilliseconds); | |||
_stopwatch.Reset(); | |||
} | |||
} | |||
public static void MonitorRemoveDuration<T>(IHandleEntityViewEngineAbstracted engine, ref T entityView) | |||
{ | |||
EngineInfo info; | |||
if (engineInfos.TryGetValue(engine.GetType(), out info)) | |||
{ | |||
_stopwatch.Start(); | |||
(engine as IHandleEntityStructEngine<T>).RemoveInternal(ref entityView); | |||
_stopwatch.Stop(); | |||
info.AddRemoveDuration(_stopwatch.Elapsed.TotalMilliseconds); | |||
_stopwatch.Reset(); | |||
} | |||
} | |||
public static void AddEngine(IEngine engine) | |||
{ | |||
if (engineInfos.ContainsKey(engine.GetType()) == false) | |||
{ | |||
engineInfos.Add(engine.GetType(), new EngineInfo(engine)); | |||
} | |||
} | |||
public static void ResetDurations() | |||
{ | |||
foreach (var engine in engineInfos) | |||
{ | |||
engine.Value.ResetDurations(); | |||
} | |||
} | |||
public static readonly Dictionary<Type, EngineInfo> engineInfos = new Dictionary<Type, EngineInfo>(); | |||
} | |||
} |
@@ -1,37 +0,0 @@ | |||
#if UNITY_EDITOR | |||
using System; | |||
using System.Collections.Generic; | |||
using UnityEngine; | |||
using UnityEngine.SceneManagement; | |||
//This profiler is based on the Entitas Visual Debugging tool | |||
//https://github.com/sschmid/Entitas-CSharp | |||
namespace Svelto.ECS.Profiler | |||
{ | |||
public class EngineProfilerBehaviour : MonoBehaviour | |||
{ | |||
public Dictionary<Type, EngineInfo>.ValueCollection engines { get { return EngineProfiler.engineInfos.Values; } } | |||
public void ResetDurations() | |||
{ | |||
EngineProfiler.ResetDurations(); | |||
} | |||
void OnEnable() | |||
{ | |||
SceneManager.sceneLoaded += OnLevelFinishedLoading; | |||
} | |||
void OnDisable() | |||
{ | |||
SceneManager.sceneLoaded -= OnLevelFinishedLoading; | |||
} | |||
void OnLevelFinishedLoading(Scene arg0, LoadSceneMode arg1) | |||
{ | |||
ResetDurations(); | |||
} | |||
} | |||
} | |||
#endif |