diff --git a/include/pitaya.h b/include/pitaya.h index 44f6493d..78d7055c 100644 --- a/include/pitaya.h +++ b/include/pitaya.h @@ -165,6 +165,11 @@ typedef struct { PC_EXPORT int pc_lib_version(void); PC_EXPORT const char* pc_lib_version_str(void); +// This version of the function returns an owned version string, +// that the application has to call free on. This is useful for interop with +// C#, since it will always free a returned string from a unmanaged function. +PC_EXPORT const char* pc_lib_version_owned_str(void); + /** * If you do use default log callback, * this function will change the level of log out. @@ -363,7 +368,6 @@ PC_EXPORT int tr_uv_tls_set_ca_file(const char* ca_file, const char* ca_path); * Macro implementation */ #define pc_lib_version() PC_VERSION_NUM -#define pc_lib_version_str() PC_VERSION_STR #ifdef __cplusplus } diff --git a/src/pc_lib.c b/src/pc_lib.c index ddcc5b26..cc1e962f 100644 --- a/src/pc_lib.c +++ b/src/pc_lib.c @@ -460,3 +460,13 @@ void pc_lib_skip_key_pin_check(bool should_skip) { pc__skip_key_pin_check = should_skip; } + +const char *pc_lib_version_str(void) +{ + return PC_VERSION_STR; +} + +const char* pc_lib_version_owned_str(void) +{ + return pc_lib_strdup(pc_lib_version_str()); +} diff --git a/unity/PitayaExample/Assets/Example.cs b/unity/PitayaExample/Assets/Example.cs index c2c3afeb..816e201d 100644 --- a/unity/PitayaExample/Assets/Example.cs +++ b/unity/PitayaExample/Assets/Example.cs @@ -2,6 +2,8 @@ using System.IO; using UnityEngine; using Pitaya; +using Pitaya.SimpleJson; +using UnityEngine.UI; public class Example : MonoBehaviour { @@ -9,11 +11,31 @@ public class Example : MonoBehaviour private bool _connected; private bool _requestSent; + public Button GetDataButton; + // Use this for initialization private void Start() { + GetDataButton.onClick.AddListener(() => + { + _client.Request("connector.getsessiondata", + action: data => + { + Debug.LogFormat("GetSessionData: {0}", data); + }, + errorAction: err => + { + Debug.LogFormat("GetSessionData Error: {0}", err); + }); + }); + // _client = new PitayaClient("ca.crt"); - _client = new PitayaClient(); + _client = new PitayaClient(new PitayaMetrics.Config(stats => + { + Debug.Log("=========> Received connection stats!"); + Debug.Log(stats.Serialize()); + }, "connector.getsessiondata")); + _connected = false; _requestSent = false; @@ -30,7 +52,7 @@ private void Start() } }; - _client.Connect("a1d127034f31611e8858512b1bea90da-838011280.us-east-1.elb.amazonaws.com", 3251, + _client.Connect("libpitaya-tests.tfgco.com", 3251, new Dictionary { {"oi", "mano"} @@ -43,12 +65,11 @@ private void Update() if (_connected && !_requestSent) { _client.Request("connector.getsessiondata", - (data) => + action: data => { Debug.Log("Got request data: " + data); - File.WriteAllText("/Users/lhahn/Downloads/OH_MY_GOD.txt", "I Got the request data: " + data); }, - (err) => + errorAction: (err) => { Debug.LogError("Got error: code = " + err.Code + ", msg = " + err.Msg); }); diff --git a/unity/PitayaExample/Assets/Example.unity b/unity/PitayaExample/Assets/Example.unity index de990268..42cbbaf1 100644 --- a/unity/PitayaExample/Assets/Example.unity +++ b/unity/PitayaExample/Assets/Example.unity @@ -13,7 +13,7 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 8 + serializedVersion: 9 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 @@ -38,7 +38,8 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.4465934, g: 0.49642956, b: 0.5748249, a: 1} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.49641287, b: 0.5748173, a: 1} + m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 @@ -49,16 +50,14 @@ LightmapSettings: m_BounceScale: 1 m_IndirectOutputScale: 1 m_AlbedoBoost: 1 - m_TemporalCoherenceThreshold: 1 m_EnvironmentLightingMode: 0 m_EnableBakedLightmaps: 1 m_EnableRealtimeLightmaps: 1 m_LightmapEditorSettings: - serializedVersion: 9 + serializedVersion: 10 m_Resolution: 2 m_BakeResolution: 40 - m_TextureWidth: 1024 - m_TextureHeight: 1024 + m_AtlasSize: 1024 m_AO: 0 m_AOMaxDistance: 1 m_CompAOExponent: 1 @@ -116,9 +115,10 @@ NavMeshSettings: --- !u!1 &376153440 GameObject: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - serializedVersion: 5 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 m_Component: - component: {fileID: 376153445} - component: {fileID: 376153444} @@ -135,38 +135,48 @@ GameObject: --- !u!114 &376153441 MonoBehaviour: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 376153440} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: a4482a4624a18440caceac740f4be357, type: 3} m_Name: m_EditorClassIdentifier: + GetDataButton: {fileID: 407976080} --- !u!81 &376153442 AudioListener: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 376153440} m_Enabled: 1 --- !u!124 &376153443 Behaviour: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 376153440} m_Enabled: 1 --- !u!20 &376153444 Camera: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 376153440} m_Enabled: 1 serializedVersion: 2 m_ClearFlags: 1 m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_GateFitMode: 2 + m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -196,8 +206,9 @@ Camera: --- !u!4 &376153445 Transform: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 376153440} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} @@ -206,12 +217,369 @@ Transform: m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &407976078 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 407976079} + - component: {fileID: 407976082} + - component: {fileID: 407976081} + - component: {fileID: 407976080} + m_Layer: 5 + m_Name: GetDataButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &407976079 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 407976078} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 517537412} + m_Father: {fileID: 1517574242} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &407976080 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 407976078} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 407976081} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &407976081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 407976078} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 +--- !u!222 &407976082 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 407976078} + m_CullTransparentMesh: 0 +--- !u!1 &517537411 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 517537412} + - component: {fileID: 517537414} + - component: {fileID: 517537413} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &517537412 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 517537411} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 407976079} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &517537413 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 517537411} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Button +--- !u!222 &517537414 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 517537411} + m_CullTransparentMesh: 0 +--- !u!1 &1517574238 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1517574242} + - component: {fileID: 1517574241} + - component: {fileID: 1517574240} + - component: {fileID: 1517574239} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1517574239 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1517574238} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1517574240 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1517574238} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!223 &1517574241 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1517574238} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1517574242 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1517574238} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 407976079} + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &1693682351 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1693682354} + - component: {fileID: 1693682353} + - component: {fileID: 1693682352} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1693682352 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693682351} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1077351063, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &1693682353 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693682351} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -619905303, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1693682354 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693682351} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1782618146 GameObject: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - serializedVersion: 5 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 m_Component: - component: {fileID: 1782618148} - component: {fileID: 1782618147} @@ -225,8 +593,9 @@ GameObject: --- !u!108 &1782618147 Light: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1782618146} m_Enabled: 1 serializedVersion: 8 @@ -252,6 +621,7 @@ Light: serializedVersion: 2 m_Bits: 4294967295 m_Lightmapping: 4 + m_LightShadowCasterMode: 0 m_AreaSize: {x: 1, y: 1} m_BounceIntensity: 1 m_ColorTemperature: 6570 @@ -261,8 +631,9 @@ Light: --- !u!4 &1782618148 Transform: m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1782618146} m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} m_LocalPosition: {x: 0, y: 3, z: 0} diff --git a/unity/PitayaExample/Assets/Pitaya/PitayaBinding.cs b/unity/PitayaExample/Assets/Pitaya/PitayaBinding.cs index d4f7b3ec..a21a83c0 100644 --- a/unity/PitayaExample/Assets/Pitaya/PitayaBinding.cs +++ b/unity/PitayaExample/Assets/Pitaya/PitayaBinding.cs @@ -69,6 +69,11 @@ private static void DLog(object data) Debug.Log(data); } } + + public static string Version + { + get { return NativeLibVersion(); } + } static PitayaBinding() { @@ -546,81 +551,82 @@ private static void LogFunction(PitayaLogLevel level, string msg) #else private const string LibName = "libpitaya-linux"; #endif + [DllImport(LibName, EntryPoint = "pc_lib_version_owned_str", CallingConvention = CallingConvention.Cdecl)] + private static extern string NativeLibVersion(); - // ReSharper disable UnusedMember.Local - [DllImport(LibName, EntryPoint = "tr_uv_tls_set_ca_file")] + [DllImport(LibName, EntryPoint = "tr_uv_tls_set_ca_file", CallingConvention = CallingConvention.Cdecl)] private static extern void NativeSetCertificatePath(string caFile, string caPath); - [DllImport(LibName, EntryPoint = "pc_unity_lib_init")] + [DllImport(LibName, EntryPoint = "pc_unity_lib_init", CallingConvention = CallingConvention.Cdecl)] private static extern void NativeLibInit(int logLevel, string caFile, string caPath, NativeAssertCallback assert, string platform, string buildNumber, string version); - [DllImport(LibName, EntryPoint = "pc_lib_set_default_log_level")] + [DllImport(LibName, EntryPoint = "pc_lib_set_default_log_level", CallingConvention = CallingConvention.Cdecl)] private static extern void NativeLibSetLogLevel(int logLevel); - [DllImport(LibName, EntryPoint = "pc_client_ev_str")] + [DllImport(LibName, EntryPoint = "pc_client_ev_str", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeEvToStr(int ev); - [DllImport(LibName, EntryPoint = "pc_client_rc_str")] + [DllImport(LibName, EntryPoint = "pc_client_rc_str", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeRcToStr(int rc); - [DllImport(LibName, EntryPoint = "pc_unity_create")] + [DllImport(LibName, EntryPoint = "pc_unity_create", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeCreate(bool enableTls, bool enablePoll, bool enableReconnect, int connTimeout); - [DllImport(LibName, EntryPoint = "pc_unity_destroy")] + [DllImport(LibName, EntryPoint = "pc_unity_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeDestroy(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_client_connect")] + [DllImport(LibName, EntryPoint = "pc_client_connect", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeConnect(IntPtr client, string host, int port, string handshakeOpts); - [DllImport(LibName, EntryPoint = "pc_client_disconnect")] + [DllImport(LibName, EntryPoint = "pc_client_disconnect", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeDisconnect(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_unity_request")] + [DllImport(LibName, EntryPoint = "pc_unity_request", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeRequest(IntPtr client, string route, string msg, uint cbUid, int timeout, NativeRequestCallback callback, NativeErrorCallback errorCallback); - [DllImport(LibName, EntryPoint = "pc_unity_binary_request")] + [DllImport(LibName, EntryPoint = "pc_unity_binary_request", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeBinaryRequest(IntPtr client, string route, byte[] data, long len, uint cbUid, int timeout, NativeRequestCallback callback, NativeErrorCallback errorCallback); - [DllImport(LibName, EntryPoint = "pc_string_notify_with_timeout")] + [DllImport(LibName, EntryPoint = "pc_string_notify_with_timeout", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeNotify(IntPtr client, string route, string msg, IntPtr exData, int timeout, NativeNotifyCallback callback); - [DllImport(LibName, EntryPoint = "pc_binary_notify_with_timeout")] + [DllImport(LibName, EntryPoint = "pc_binary_notify_with_timeout", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeBinaryNotify(IntPtr client, string route, byte[] data, long len, IntPtr exData, int timeout, NativeNotifyCallback callback); - [DllImport(LibName, EntryPoint = "pc_client_poll")] + [DllImport(LibName, EntryPoint = "pc_client_poll", CallingConvention = CallingConvention.Cdecl)] private static extern int NativePoll(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_client_add_ev_handler")] + [DllImport(LibName, EntryPoint = "pc_client_add_ev_handler", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeAddEventHandler(IntPtr client, NativeEventCallback callback, IntPtr exData, IntPtr destructor); - [DllImport(LibName, EntryPoint = "pc_client_set_push_handler")] + [DllImport(LibName, EntryPoint = "pc_client_set_push_handler", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeAddPushHandler(IntPtr client, NativePushCallback callback); - [DllImport(LibName, EntryPoint = "pc_client_rm_ev_handler")] + [DllImport(LibName, EntryPoint = "pc_client_rm_ev_handler", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeRemoveEventHandler(IntPtr client, int handlerId); - [DllImport(LibName, EntryPoint = "pc_client_conn_quality")] + [DllImport(LibName, EntryPoint = "pc_client_conn_quality", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeQuality(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_client_state")] + [DllImport(LibName, EntryPoint = "pc_client_state", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeState(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_client_serializer")] + [DllImport(LibName, EntryPoint = "pc_client_serializer", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeSerializer(IntPtr client); - [DllImport(LibName, EntryPoint = "pc_client_free_serializer")] + [DllImport(LibName, EntryPoint = "pc_client_free_serializer", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeFreeSerializer(IntPtr serializer); // ReSharper restore UnusedMember.Local - [DllImport(LibName, EntryPoint = "pc_lib_add_pinned_public_key_from_certificate_string")] + [DllImport(LibName, EntryPoint = "pc_lib_add_pinned_public_key_from_certificate_string", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeAddPinnedPublicKeyFromCertificateString(string ca_string); - [DllImport(LibName, EntryPoint = "pc_lib_add_pinned_public_key_from_certificate_file")] + [DllImport(LibName, EntryPoint = "pc_lib_add_pinned_public_key_from_certificate_file", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeAddPinnedPublicKeyFromCertificateFile(string caPath); - [DllImport(LibName, EntryPoint = "pc_lib_skip_key_pin_check")] + [DllImport(LibName, EntryPoint = "pc_lib_skip_key_pin_check", CallingConvention = CallingConvention.Cdecl)] private static extern void NativeSkipKeyPinCheck(bool shouldSkip); - [DllImport(LibName, EntryPoint = "pc_lib_clear_pinned_public_keys")] + [DllImport(LibName, EntryPoint = "pc_lib_clear_pinned_public_keys", CallingConvention = CallingConvention.Cdecl)] private static extern void NativeClearPinnedPublicKeys(); - [DllImport(LibName, EntryPoint = "pc_unity_init_log_function")] + [DllImport(LibName, EntryPoint = "pc_unity_init_log_function", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeInitLogFunction(NativeLogFunction fn); #if UNITY_IPHONE && !UNITY_EDITOR [DllImport("__Internal")] diff --git a/unity/PitayaExample/Assets/Pitaya/PitayaClient.cs b/unity/PitayaExample/Assets/Pitaya/PitayaClient.cs index 66221d2e..6081fee8 100644 --- a/unity/PitayaExample/Assets/Pitaya/PitayaClient.cs +++ b/unity/PitayaExample/Assets/Pitaya/PitayaClient.cs @@ -10,9 +10,10 @@ public class PitayaClient : IDisposable, IPitayaListener { public event Action NetWorkStateChangedEvent; - private const int DEFAULT_CONNECTION_TIMEOUT = 30; + private const int DefaultConnectionTimeout = 30; private IntPtr _client = IntPtr.Zero; + private PitayaMetrics _metricsAggr; private EventManager _eventManager; private bool _disposed; private uint _reqUid; @@ -22,22 +23,27 @@ public class PitayaClient : IDisposable, IPitayaListener public PitayaClient() { - Init(null, false, false, false, DEFAULT_CONNECTION_TIMEOUT); + Init(null, false, false, false, DefaultConnectionTimeout, null); } public PitayaClient(int connectionTimeout) { - Init(null, false, false, false, connectionTimeout); + Init(null, false, false, false, connectionTimeout, null); } public PitayaClient(string certificateName = null) { - Init(certificateName, certificateName != null, false, false, DEFAULT_CONNECTION_TIMEOUT); + Init(certificateName, certificateName != null, false, false, DefaultConnectionTimeout, null); } - public PitayaClient(bool enableReconnect = false, string certificateName = null, int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT) + public PitayaClient(PitayaMetrics.Config config = null) { - Init(certificateName, certificateName != null, false, enableReconnect, DEFAULT_CONNECTION_TIMEOUT); + Init(null, false, false, false, DefaultConnectionTimeout, config); + } + + public PitayaClient(bool enableReconnect = false, string certificateName = null, int connectionTimeout = DefaultConnectionTimeout, PitayaMetrics.Config config = null) + { + Init(certificateName, certificateName != null, false, enableReconnect, DefaultConnectionTimeout, config); } ~PitayaClient() @@ -45,13 +51,24 @@ public PitayaClient(bool enableReconnect = false, string certificateName = null, Dispose(); } - private void Init(string certificateName, bool enableTlS, bool enablePolling, bool enableReconnect, int connTimeout) + private void Init( + string certificateName, + bool enableTlS, + bool enablePolling, + bool enableReconnect, + int connTimeout, + PitayaMetrics.Config config) { _eventManager = new EventManager(); _typeRequestSubscriber = new TypeSubscriber(); _typePushSubscriber = new TypeSubscriber(); _client = PitayaBinding.CreateClient(enableTlS, enablePolling, enableReconnect, connTimeout, this); + if (config != null) + { + _metricsAggr = new PitayaMetrics(config); + } + if (certificateName != null) { #if UNITY_EDITOR @@ -83,11 +100,13 @@ public PitayaClientState State public void Connect(string host, int port, string handshakeOpts = null) { + if (_metricsAggr != null) _metricsAggr.Start(); PitayaBinding.Connect(_client, host, port, handshakeOpts); } public void Connect(string host, int port, Dictionary handshakeOpts) { + if (_metricsAggr != null) _metricsAggr.Start(); var opts = Pitaya.SimpleJson.SimpleJson.SerializeObject(handshakeOpts); PitayaBinding.Connect(_client, host, port, opts); } @@ -114,12 +133,24 @@ public void Request(string route, IMessage msg, Action action, Action(string route, IMessage msg, int timeout, Action action, Action errorAction) { + if (_metricsAggr != null) _metricsAggr.StartRecordingRequest(route); + _reqUid++; _typeRequestSubscriber.Subscribe(_reqUid, typeof(T)); - Action responseAction = res => { action((T) res); }; + void ResponseAction(object res) + { + if (_metricsAggr != null) _metricsAggr.StopRecordingRequest(route); + action((T) res); + } + + void ErrorAction(PitayaError err) + { + if (_metricsAggr != null) _metricsAggr.StopRecordingRequest(route, err); + errorAction(err); + } - _eventManager.AddCallBack(_reqUid, responseAction, errorAction); + _eventManager.AddCallBack(_reqUid, ResponseAction, ErrorAction); var serializer = PitayaBinding.ClientSerializer(_client); @@ -128,10 +159,23 @@ public void Request(string route, IMessage msg, int timeout, Action action public void Request(string route, string msg, int timeout, Action action, Action errorAction) { + if (_metricsAggr != null) _metricsAggr.StartRecordingRequest(route); + _reqUid++; - Action responseAction = res => { action((string) res); }; - _eventManager.AddCallBack(_reqUid, responseAction, errorAction); + void ResponseAction(object res) + { + _metricsAggr.StopRecordingRequest(route); + action((string) res); + } + + void ErrorAction(PitayaError err) + { + _metricsAggr.StopRecordingRequest(route, err); + errorAction(err); + } + + _eventManager.AddCallBack(_reqUid, ResponseAction, ErrorAction); PitayaBinding.Request(_client, route,JsonSerializer.Encode(msg), _reqUid, timeout); } @@ -159,17 +203,25 @@ public void Notify(string route, int timeout, string msg) public void OnRoute(string route, Action action) { - Action responseAction = res => { action((string) res); }; - _eventManager.AddOnRouteEvent(route, responseAction); + void ResponseAction(object res) + { + action((string) res); + } + + _eventManager.AddOnRouteEvent(route, ResponseAction); } // start listening to a route public void OnRoute(string route, Action action) { _typePushSubscriber.Subscribe(route, typeof(T)); - Action responseAction = res => { action((T) res); }; - _eventManager.AddOnRouteEvent(route, responseAction); + void ResponseAction(object res) + { + action((T) res); + } + + _eventManager.AddOnRouteEvent(route, ResponseAction); } public void OffRoute(string route) @@ -207,6 +259,7 @@ public void OnRequestError(uint rid, PitayaError error) public void OnNetworkEvent(PitayaNetWorkState state, NetworkError error) { + if (_metricsAggr != null) _metricsAggr.Update(state, error); if(NetWorkStateChangedEvent != null ) NetWorkStateChangedEvent.Invoke(state, error); } @@ -223,7 +276,7 @@ public void OnUserDefinedPush(string route, byte[] serializedBody) decoded = JsonSerializer.Decode(serializedBody); } - _eventManager.InvokeOnEvent(route, decoded); + _eventManager.InvokeOnEvent(route, decoded); } public void Dispose() @@ -236,6 +289,7 @@ public void Dispose() _reqUid = 0; PitayaBinding.Disconnect(_client); + if (_metricsAggr != null) _metricsAggr.ForceStop(); PitayaBinding.Dispose(_client); _client = IntPtr.Zero; diff --git a/unity/PitayaExample/Assets/Pitaya/PitayaConstants.cs b/unity/PitayaExample/Assets/Pitaya/PitayaConstants.cs index 6dff8b52..2369ed86 100644 --- a/unity/PitayaExample/Assets/Pitaya/PitayaConstants.cs +++ b/unity/PitayaExample/Assets/Pitaya/PitayaConstants.cs @@ -30,12 +30,9 @@ public NetworkError(string error, string description) public enum PitayaNetWorkState { - Closed, - Connecting, FailToConnect, Connected, Disconnected, - Timeout, Error, Kicked } diff --git a/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs b/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs new file mode 100644 index 00000000..53bdc315 --- /dev/null +++ b/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs @@ -0,0 +1,414 @@ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Assertions; + +namespace Pitaya +{ + public class PitayaMetrics + { + public delegate void MetricsCallback(ConnectionSessionStats stats); + + public class Config + { + private MetricsCallback _cb; + private string _pingRoute; + + public string PingRoute { get { return _pingRoute; } } + public MetricsCallback Cb { get { return _cb; } } + + public Config(MetricsCallback cb, string pingRoute = null) + { + Assert.IsNotNull(cb); + _cb = cb; + _pingRoute = pingRoute; + } + } + + private class RequestRecording + { + public List LatenciesMs = new List(); + public Stopwatch Watch = new Stopwatch(); + } + + private class PingRecording + { + public List LatenciesMs = new List(); + public Stopwatch Watch = new Stopwatch(); + public uint Loss; + public uint Total; + } + + private enum State + { + NotConnected, + Connecting, + Connected + } + + private class ConnectingState + { + public Stopwatch ConnectionWatch = new Stopwatch(); + } + + private class ConnectedState + { + public Stopwatch SessionWatch = new Stopwatch(); + public bool KickReceived; + } + + // The current version of the event that is being sent. This value should always increase when the + // format of the struct changes. + private const uint EventVersion = 1; + + // The current state of the pitaya connection. + private State _state; + private ConnectedState _connectedState; + private ConnectingState _connectingState; + private readonly Dictionary _requestsLatencies; + private readonly PingRecording _pingRecording; + private readonly Config _config; + + private ConnectionSessionStats _connectionSessionStats; + + private static class ConnectionFinishReason + { + public const string UserRequest = "UserRequest"; + public const string FailedToConnect = "FailedToConnect"; + public const string ConnectionError = "ConnectionError"; + public const string Kick = "Kick"; + public const string UnknownError = "UnknownError"; + } + + public struct ConnectionSessionStats + { + // TODO(lhahn): Consider the case where multiple clients are created, should a session contain an ID? + // Or should a pitaya client contain an id as well to distinguish different client instances? + public uint Version; + public double SessionDurationSec; + public double PingAverage; + public double PingStdDeviation; + public uint PingTotal; + public uint PingLoss; + public string ConnectionFinishReason; + public string ConnectionFinishDetails; + public double ConnectionTimeMs; + public string ConnectionRegion; + public Dictionary RoutesLatencyMs; + public Dictionary RoutesStandardDeviation; + public string NetworkType; + public string LibPitayaVersion; + public uint ServerInvalidPackages; + + public string Serialize() + { + return SimpleJson.SimpleJson.SerializeObject(this); + } + } + + public PitayaMetrics(Config config) + { + Assert.IsNotNull(config); + _config = config; + _state = State.NotConnected; + _connectedState = null; + _connectingState = null; + _pingRecording = new PingRecording(); + _requestsLatencies = new Dictionary(15); + } + + public void Start() + { + _connectionSessionStats = DefaultConnectionSessionStats(); + _state = State.Connecting; + _connectingState = new ConnectingState + { + ConnectionWatch = new Stopwatch() + }; + _connectingState.ConnectionWatch.Start(); + Assert.IsNull(_connectedState); + } + + public void StartRecordingRequest(string route) + { + // We should not assume here that the _state variable will be of a specific value. LibPitaya can buffer + // requests even before the client is connected. + if (route == _config.PingRoute) + { + _pingRecording.Watch.Start(); + } + else + { + if (_requestsLatencies.TryGetValue(route, out RequestRecording recording)) + { + recording.Watch.Start(); + } + else + { + var r = new RequestRecording(); + _requestsLatencies.Add(route, r); + r.Watch.Start(); + } + } + } + + public void StopRecordingRequest(string route, PitayaError err = null) + { + if (err != null) + { + // TODO(lhahn): Should some errors not be collected here? For example, timeouts. + // for the moment just ignore errors... + } + + if (route == _config.PingRoute) + { + _pingRecording.Watch.Stop(); + _pingRecording.LatenciesMs.Add(_pingRecording.Watch.Elapsed.TotalMilliseconds); + _pingRecording.Watch.Reset(); + _pingRecording.Total++; + if (err != null && err.Code == "PC_RC_TIMEOUT") + { + _pingRecording.Loss++; + } + } + else + { + Assert.IsTrue(_requestsLatencies.ContainsKey(route)); + if (_requestsLatencies.TryGetValue(route, out RequestRecording recording)) + { + recording.Watch.Stop(); + recording.LatenciesMs.Add(recording.Watch.Elapsed.TotalMilliseconds); + recording.Watch.Reset(); + } + } + } + + public void Update(PitayaNetWorkState pitayaState, NetworkError error) + { + switch (_state) + { + case State.NotConnected: + UpdateNotConnectedState(pitayaState, error); + break; + case State.Connecting: + UpdateConnectingState(pitayaState, error); + break; + case State.Connected: + UpdateConnectedState(pitayaState, error); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void ForceStop() + { + // HACK(lhahn): We simulate a disconnect to the metrics aggregator. This is necessary because the dispose is called + // before the disconnect event can be fired in the PitayaClient class. This could be resolved in the future, + // but for the moment I think this solution won't have issues. + Update(PitayaNetWorkState.Disconnected, null); + } + + private void UpdateConnectedState(PitayaNetWorkState pitayaState, NetworkError pitayaErr) + { + Assert.IsTrue(_state == State.Connected); + Assert.IsNotNull(_connectedState); + Assert.IsNull(_connectingState); + + switch (pitayaState) + { + case PitayaNetWorkState.Kicked: + // LibPitaya sends a Kicked event and after that a Disconnected event. Therefore, + // we do not close the session yet, we just signal that a kick was received. + _connectedState.KickReceived = true; + break; + case PitayaNetWorkState.Disconnected: + StopConnectedState(pitayaErr); + break; + case PitayaNetWorkState.Error: + // This event only happens when unknown data from the server was sent, so we just increment the counter. + _connectionSessionStats.ServerInvalidPackages++; + break; + default: + throw new Exception(string.Format("PitayaMetrics received pitaya state {0} when in not connected state", pitayaState)); + } + } + + private void UpdateNotConnectedState(PitayaNetWorkState pitayaState, NetworkError pitayaErr) + { + Assert.IsTrue(_state == State.NotConnected); + Assert.IsNull(_connectedState); + Assert.IsNull(_connectingState); + + // It is possible to receive a duplicated disconnected event, so we just ignore it. + if (pitayaState != PitayaNetWorkState.Disconnected) + { + throw new Exception(string.Format("PitayaMetrics received pitaya state {0} when in not connected state", pitayaState)); + } + } + + private void UpdateConnectingState(PitayaNetWorkState pitayaState, NetworkError pitayaErr) + { + Assert.IsTrue(_state == State.Connecting); + Assert.IsNull(_connectedState); + Assert.IsNotNull(_connectingState); + + switch (pitayaState) + { + case PitayaNetWorkState.Connected: + // If the connection connected successfully, we start the session stopwatch and + // stop the connection watch. + _connectingState.ConnectionWatch.Stop(); + _connectionSessionStats.ConnectionTimeMs = _connectingState.ConnectionWatch.Elapsed.TotalMilliseconds; + _connectingState.ConnectionWatch.Reset(); + _connectingState = null; + + _state = State.Connected; + _connectedState = new ConnectedState(); + _connectedState.SessionWatch.Start(); + break; + case PitayaNetWorkState.FailToConnect: + // If the connection failed while we were trying to connect, we should close the session with + // this information. + StopConnectingState(pitayaErr); + break; + case PitayaNetWorkState.Error: + // This event only happens when unknown data from the server was sent, so we just increment the counter. + _connectionSessionStats.ServerInvalidPackages++; + break; + default: + throw new Exception(string.Format("PitayaMetrics received pitaya state {0} when in not connected state", pitayaState)); + } + } + + private void StopConnectingState(NetworkError pitayaErr) + { + Assert.IsNotNull(pitayaErr); + Assert.IsNull(_connectedState); + Assert.IsNotNull(_connectingState); + _connectionSessionStats.ConnectionFinishReason = ConnectionFinishReason.FailedToConnect; + _connectionSessionStats.ConnectionFinishDetails = GetErrorDetails(pitayaErr); + CalculateRoutesAndPingMetrics(ref _connectionSessionStats); + + SendConnectionStatsSummaryAndResetState(); + } + + private void StopConnectedState(NetworkError pitayaErr) + { + Assert.IsNotNull(_connectedState); + Assert.IsNull(_connectingState); + + if (pitayaErr == null) + { + _connectionSessionStats.ConnectionFinishReason = _connectedState.KickReceived + ? ConnectionFinishReason.Kick + : ConnectionFinishReason.UserRequest; + } + else + { + _connectionSessionStats.ConnectionFinishReason = ConnectionFinishReason.ConnectionError; + _connectionSessionStats.ConnectionFinishDetails = GetErrorDetails(pitayaErr); + } + + _connectedState.SessionWatch.Stop(); + _connectionSessionStats.SessionDurationSec = _connectedState.SessionWatch.Elapsed.TotalSeconds; + CalculateRoutesAndPingMetrics(ref _connectionSessionStats); + + SendConnectionStatsSummaryAndResetState(); + } + + private void SendConnectionStatsSummaryAndResetState() + { + _config.Cb(_connectionSessionStats); + _connectionSessionStats = DefaultConnectionSessionStats(); + _state = State.NotConnected; + _connectingState = null; + _connectedState = null; + // TODO(lhahn): consider not clearing the dictionary here, since the routes will probably be reused anyways. + _requestsLatencies.Clear(); + } + + private void CalculateRoutesAndPingMetrics(ref ConnectionSessionStats connectionSessionStats) + { + Assert.IsTrue(connectionSessionStats.RoutesLatencyMs.Count == 0); + Assert.IsTrue(connectionSessionStats.RoutesStandardDeviation.Count == 0); + + double CalculateAverage(List arr) + { + if (arr.Count == 0) + return 0; + + double average = 0; + for (var i = 0; i < arr.Count; ++i) + { + average += arr[i]; + } + average /= arr.Count; + return average; + } + + double CalculateStdDeviation(List arr, double avg) + { + if (arr.Count == 0) + return 0; + + double stdDeviation = 0; + for (var i = 0; i < arr.Count; ++i) + { + stdDeviation += Math.Pow(arr[i] - avg, 2); + } + stdDeviation /= arr.Count; + stdDeviation = Math.Sqrt(stdDeviation); + return stdDeviation; + } + + foreach (KeyValuePair kv in _requestsLatencies) + { + double averageLatency = CalculateAverage(kv.Value.LatenciesMs); + double stdDeviation = CalculateStdDeviation(kv.Value.LatenciesMs, averageLatency); + + connectionSessionStats.RoutesLatencyMs.Add(kv.Key, averageLatency); + connectionSessionStats.RoutesStandardDeviation.Add(kv.Key, stdDeviation); + } + + connectionSessionStats.PingAverage = CalculateAverage(_pingRecording.LatenciesMs); + connectionSessionStats.PingStdDeviation = CalculateStdDeviation( + _pingRecording.LatenciesMs, connectionSessionStats.PingAverage + ); + connectionSessionStats.PingTotal = _pingRecording.Total; + connectionSessionStats.PingLoss = _pingRecording.Loss; + } + + private static ConnectionSessionStats DefaultConnectionSessionStats() + { + return new ConnectionSessionStats + { + Version = EventVersion, + NetworkType = GetNetworkType(), + LibPitayaVersion = PitayaBinding.Version, + // TODO(lhahn): remove hardcoded region here and use something better. + ConnectionRegion = "NA", + RoutesLatencyMs = new Dictionary(), + RoutesStandardDeviation = new Dictionary() + }; + } + + private static string GetNetworkType() + { + switch (Application.internetReachability) + { + case NetworkReachability.NotReachable: return "not-reachable"; + case NetworkReachability.ReachableViaCarrierDataNetwork: return "data"; + case NetworkReachability.ReachableViaLocalAreaNetwork: return "wifi"; + default: throw new ArgumentOutOfRangeException(); + } + } + + private static string GetErrorDetails(NetworkError e) + { + Assert.IsNotNull(e, "error should not be null"); + return string.Format("{0}: {1}", e.Error, e.Description); + } + } +} diff --git a/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs.meta b/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs.meta new file mode 100644 index 00000000..67e921a5 --- /dev/null +++ b/unity/PitayaExample/Assets/Pitaya/PitayaMetrics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a081eb48df302442e8b66553ade668fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PitayaExample/Assets/Tests/MetricsTest.cs b/unity/PitayaExample/Assets/Tests/MetricsTest.cs new file mode 100644 index 00000000..d0da7ae1 --- /dev/null +++ b/unity/PitayaExample/Assets/Tests/MetricsTest.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Pitaya.Tests +{ + public class PitayaMetricsTest + { + const string ServerHost = "libpitaya-tests.tfgco.com"; + const string GetSessionDataRoute = "connector.getsessiondata"; + const int ServerPort = 3251; + + PitayaClient _client; + + [SetUp] + public void Setup() { } + + [TearDown] + public void TearDown() + { + if (_client == null) return; + _client.Disconnect(); + _client.Dispose(); + _client = null; + } + + static IEnumerator Connect(PitayaClient client) + { + var called = false; + var connectionState = PitayaNetWorkState.Disconnected; + + client.NetWorkStateChangedEvent += (networkState, error) => + { + called = true; + connectionState = networkState; + }; + + client.Connect(ServerHost, ServerPort); + + while (!called) + { + yield return new WaitForSeconds(0.2f); + } + + Assert.True(called); + Assert.AreEqual(connectionState, PitayaNetWorkState.Connected); + } + + [UnityTest] + public IEnumerator StatsShouldBeReportedAtEndOfConnection() + { + bool statsCalled = false; + + _client = new PitayaClient(new PitayaMetrics.Config(stats => + { + statsCalled = true; + Assert.Equals(stats.ConnectionFinishReason, "UserRequest"); + })); + yield return Connect(_client); + _client.Disconnect(); + yield return new WaitForSeconds(0.2f); + Assert.IsTrue(statsCalled); + } + + [UnityTest] + public IEnumerator PingStatsShouldBeReportedOnlyWhenRouteIsCalled() + { + { + bool statsCalled = false; + + _client = new PitayaClient(new PitayaMetrics.Config( + stats => + { + statsCalled = true; + Assert.Less(Math.Abs(stats.PingAverage), 0.0001); + Assert.Less(Math.Abs(stats.PingStdDeviation), 0.0001); + Assert.Equals(stats.PingTotal, 0); + Assert.Equals(stats.PingLoss, 0); + }, + GetSessionDataRoute + )); + + yield return Connect(_client); + _client.Disconnect(); + yield return new WaitForSeconds(0.3f); + Assert.IsTrue(statsCalled); + } + } + } +} diff --git a/unity/PitayaExample/Assets/Tests/MetricsTest.cs.meta b/unity/PitayaExample/Assets/Tests/MetricsTest.cs.meta new file mode 100644 index 00000000..60b5269b --- /dev/null +++ b/unity/PitayaExample/Assets/Tests/MetricsTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 300dd9a6ad8541e9aa39e334e08f72f6 +timeCreated: 1575925345 \ No newline at end of file