#define LINEAR_OPT

using System.Threading;
using System;
using System.Collections;
using System.Diagnostics;
using System.Net;
using Beefy;
using System.IO;

namespace BeefPerf
{
	enum BpCmd : uint8
	{
		Init,
		Enter,
		EnterDyn,
		Leave,
		LODSmallEntry, 
		StrEntry,
		ThreadAdd,
		ThreadRemove,
		SetThread,
		ThreadName,
		FrameTick,
		PrevFrameTick,
		KeepAlive,
		ClockInfo,
		StreamSplitInfo,
		Event,
		LODEvent,
		Cmd,
	}

	class BpStreamData
	{
		public List<uint8> mBuffer ~ delete _;

		public int64 mStartTick;
		public int64 mEndTick;
		public int64 mSplitTick;
		public int64 mSizeOverride = -1; // To reserve size to close out entries
		public int32 mPendingLeaveDepth;
		public int32 mNumZones;
		public int64 mMinTicks;

		public void Write(uint8 val)
		{
			mBuffer.Add(val);
		}

		public void Write(void* ptr, int size)
		{
			int curSize = mBuffer.Count;
			mBuffer.GrowUnitialized((int32)size);
			var destPtr = &mBuffer[curSize];
			Internal.MemCpy(destPtr, ptr, size);
		}

		public void WriteTickDelta(int64 tick)
		{
			int64 encodedDelta = (tick - mEndTick) / BpClient.cBufTickScale;
			WriteSLEB128(encodedDelta);
			mEndTick += encodedDelta * BpClient.cBufTickScale;
		}

		public void WriteSLEB128(int64 value)
		{
			int64 valLeft = value;
			bool hasMore;
			repeat
			{
				uint8 curByte = (uint8)(valLeft & 0x7f);    
				valLeft >>= 7;
				hasMore = !((((valLeft == 0) && ((curByte & 0x40) == 0)) ||
					((valLeft == -1) && ((curByte & 0x40) != 0))));
				if (hasMore)
					curByte |= 0x80;
				mBuffer.Add(curByte);
			}
			while (hasMore);        
		}
	}

	class BpStreamLOD
	{
		struct LODEntry
		{
			public int32 mZoneNameId;
			public int32 mParamsReadPos;
			public int64 mStartTick;
			public bool mWritten;
		}

		struct LODSmallEntry
		{
			public int64 mStartTick;
			public int64 mEndTick;
		}

		public int64 mMinTicks;
		public List<BpStreamData> mStreamDataList = new List<BpStreamData>() ~ DeleteContainerAndItems!(_);

		void FlushSmallEntry(BpStreamData destStreamData, List<LODSmallEntry> smallEntryStack, int32 stackDepth)
		{
			if (stackDepth >= smallEntryStack.Count)
				return;
			var smallEntry = ref smallEntryStack[stackDepth];
			if (smallEntry.mStartTick == 0)
				return;

			destStreamData.Write((uint8)BpCmd.LODSmallEntry);
			destStreamData.WriteTickDelta(smallEntry.mStartTick);
			destStreamData.WriteTickDelta(smallEntry.mEndTick);
			destStreamData.WriteSLEB128(stackDepth);

			smallEntry.mStartTick = 0;
		}

		void FlushLODEntry(BpCmdTarget cmdTarget, BpStreamData srcStreamData, BpStreamData destStreamData, List<LODEntry> entryStack, List<LODSmallEntry> smallEntryList)
		{
			int32 stackDepth;
			for (stackDepth = (int32)entryStack.Count - 1; stackDepth >= 0; stackDepth--)
			{
				var entry = ref entryStack[stackDepth];
				if (entry.mWritten)
					break;
			}
			stackDepth++;

			while (stackDepth < entryStack.Count)
			{
				FlushSmallEntry(destStreamData, smallEntryList, stackDepth);
				var entry = ref entryStack[stackDepth];
				destStreamData.mNumZones++;

				destStreamData.Write((uint8)BpCmd.Enter);
				destStreamData.WriteTickDelta(entry.mStartTick);
				destStreamData.WriteSLEB128(entry.mZoneNameId);

				int32 paramSize = 0;
				
				if (entry.mZoneNameId >= 0)
				{
					let zoneName = cmdTarget.mSession.mZoneNames[entry.mZoneNameId];
					paramSize = zoneName.mParamsSize;
				}
				else if (entry.mZoneNameId < 0)
				{
					int32 nameLen = -entry.mZoneNameId;
					destStreamData.Write(&srcStreamData.mBuffer[entry.mParamsReadPos - nameLen], nameLen);
					paramSize = -1;
				}

				int paramReadPos = entry.mParamsReadPos;
				if (paramSize == -1)
				{
					uint8* paramPtr = &srcStreamData.mBuffer[paramReadPos];
					uint8* readPtr = paramPtr;
					paramSize = (int32)Utils.DecodeInt64(ref readPtr);
					destStreamData.WriteSLEB128(paramSize);
					paramReadPos += readPtr - paramPtr;
				}

				if (paramSize != 0)
				{
					destStreamData.Write(&srcStreamData.mBuffer[paramReadPos], paramSize);
				}

				entry.mWritten = true;
				stackDepth++;
			}
		}

		public BpStreamData DoGenerate(BpCmdTarget cmdTarget, List<BpStreamData> srcStreamDataList, int32 idx, int64 overrideMinTicks = -1)
		{
			int64 minTicks = (overrideMinTicks != -1) ? overrideMinTicks : mMinTicks;
			if (minTicks == -1)
				minTicks = mMinTicks;

			if (idx <= mStreamDataList.Count)
			{
				if (mStreamDataList.Capacity == 0)
					mStreamDataList.Capacity = srcStreamDataList.Count;
				else
					mStreamDataList.EnsureCapacity(srcStreamDataList.Count, true);
				mStreamDataList.GrowUnitialized(idx - (int32)mStreamDataList.Count + 1);
			}

			let tempData = scope List<uint8>(srcStreamDataList.Count);

			let srcStreamData = srcStreamDataList[idx];
			let destStreamData = new BpStreamData();
			destStreamData.mMinTicks = minTicks;
			destStreamData.mBuffer = tempData;
			destStreamData.mStartTick = srcStreamData.mStartTick;
			destStreamData.mEndTick = destStreamData.mStartTick; // Init to startTick for proper encoding of tick deltas
			destStreamData.mSplitTick = srcStreamData.mSplitTick;
			mStreamDataList[idx] = destStreamData;

			BPStateContext stateCtx = scope BPStateContext(cmdTarget.mSession, srcStreamData);
			stateCtx.mAutoLeave = false;

			List<LODEntry> entryStack = scope List<LODEntry>();
			List<LODSmallEntry> smallEntryStack = scope List<LODSmallEntry>();

			int readCount = 0;

			int64 lastEventTick = 0;

			CmdLoop: while (true)
			{
				switch (stateCtx.GetNextEvent())
				{
				case let .Enter(startTick, strIdx):
					readCount++;
					LODEntry entry;
					entry.mStartTick = startTick;
					entry.mZoneNameId = strIdx;
					entry.mParamsReadPos = stateCtx.ReadPos;
					entry.mWritten = false;
					entryStack.Add(entry);
				case let .Leave(endTick):
					int32 stackDepth = (int32)entryStack.Count - 1;
					var entry = ref entryStack[stackDepth];

					int64 ticks = endTick - entry.mStartTick;
					// Keep either long or spanning-from-previous entries
					if ((ticks >= minTicks) || (entry.mStartTick <= srcStreamData.mStartTick))
					{
						FlushLODEntry(cmdTarget, srcStreamData, destStreamData, entryStack, smallEntryStack);
					}
					else
					{
						while (stackDepth >= smallEntryStack.Count)
							smallEntryStack.Add(LODSmallEntry());
						var smallEntry = ref smallEntryStack[stackDepth];
						if (smallEntry.mStartTick != 0)
						{
							int64 spaceTicks = entry.mStartTick - smallEntry.mEndTick;
							Debug.Assert(spaceTicks >= 0);
							if (spaceTicks > minTicks)
							{
                                FlushSmallEntry(destStreamData, smallEntryStack, stackDepth);
							}
						}
						if (smallEntry.mStartTick == 0)
							smallEntry.mStartTick = entry.mStartTick;
						Debug.Assert(endTick >= smallEntry.mEndTick);
						smallEntry.mEndTick = endTick;
					}

					if (entry.mWritten)
					{
						destStreamData.Write((uint8)BpCmd.Leave);
						destStreamData.WriteTickDelta(endTick);

						/*int readPos = stateCtx.ReadPos;
						int writePos = destStreamData.mBuffer.Count;
						Debug.Assert(readPos == writePos);*/
					}

					entryStack.RemoveAt(stackDepth);
				case .EndOfStream:
					break CmdLoop;
				case .FrameTick(int64 tick):
					destStreamData.Write((uint8)BpCmd.FrameTick);
					destStreamData.WriteTickDelta(tick);
				case .PrevFrameTick(int64 tick):
					destStreamData.Write((uint8)BpCmd.PrevFrameTick);
					destStreamData.WriteTickDelta(tick);
				case let .LODSmallEntry(startTick, endTick, stackDepth):
					while (stackDepth >= smallEntryStack.Count)
						smallEntryStack.Add(LODSmallEntry());
					var smallEntry = ref smallEntryStack[stackDepth];
					if (smallEntry.mStartTick != 0)
					{
						int64 spaceTicks = startTick - smallEntry.mEndTick;
						Debug.Assert(spaceTicks >= 0);
						if (spaceTicks > minTicks)
						{
					        FlushSmallEntry(destStreamData, smallEntryStack, stackDepth);
						}
					}
					if (smallEntry.mStartTick == 0)
						smallEntry.mStartTick = startTick;
					Debug.Assert(endTick >= smallEntry.mEndTick);
					smallEntry.mEndTick = endTick;
				case let .Event(tick, name, details):
					if (tick - lastEventTick >= minTicks)
					{
						destStreamData.Write((uint8)BpCmd.LODEvent);
						destStreamData.WriteTickDelta(tick);
						destStreamData.WriteSLEB128(stateCtx.mParamsPtr - stateCtx.mReadStart);
						lastEventTick = tick;
					}
				case let .LODEvent(tick, paramsOfs):
					if (tick - lastEventTick >= minTicks)
					{
						destStreamData.Write((uint8)BpCmd.LODEvent);
						destStreamData.WriteTickDelta(tick);
						destStreamData.WriteSLEB128(paramsOfs);
						lastEventTick = tick;
					}
				default:
					Runtime.FatalError();
				}
			}

			FlushLODEntry(cmdTarget, srcStreamData, destStreamData, entryStack, smallEntryStack);
			for (int32 stackDepth < (int32)smallEntryStack.Count)
				FlushSmallEntry(destStreamData, smallEntryStack, stackDepth);

			int sizeAdd = (int)(srcStreamData.mSizeOverride - srcStreamData.mBuffer.Count);
			int wantSize = destStreamData.mBuffer.Count + sizeAdd;
			destStreamData.mSizeOverride = wantSize;

			let outData = new List<uint8>((int32)wantSize);
			tempData.CopyTo(outData);
			destStreamData.mBuffer = outData;

			// Validate stream
			/*if (true)
			{
				BPStateContext validateStateCtx = scope BPStateContext(cmdTarget.mClient, destStreamData);
				CmdLoop: while (true)
				{
					switch (validateStateCtx.GetNextEvent())
					{
					case .EndOfStream:
						break CmdLoop;
					default:
					}
				}

				//let srcStreamData = srcStreamDataList[idx];
				if (validateStateCtx.mCurTick != destStreamData.mEndTick)
				{
					Runtime.FatalError();
				}
			}*/

			return destStreamData;
		}

		public BpStreamData Generate(BpCmdTarget cmdTarget, List<BpStreamData> srcStreamDataList, int32 idx, int64 overrideMinTicks = -1)
		{
			var streamData = DoGenerate(cmdTarget, srcStreamDataList, idx, overrideMinTicks);

			

			return streamData;
		}
	}

	class BpCmdTarget
	{
		public BpSession mSession;
		public int32 mNativeThreadId;
		public int64 mCreatedTick;
		public int64 mRemoveTick;

		//public List<BPEntry> mEntries = new List<BPEntry>() ~ delete _;
		//public List<int> mStack = new List<int>();

		public List<BpStreamLOD> mStreamLODs = new List<BpStreamLOD>() ~ DeleteContainerAndItems!(_);
		public List<BpStreamData> mStreamDataList = new List<BpStreamData>() ~ DeleteContainerAndItems!(_);
		public BpStreamData mStreamData;

		public List<BPEntry> mEntryStack = new List<BPEntry>() ~ delete _;
		public int64 mLastFrameTick;

		public this(BpSession session, bool defaultInit)
		{
			mSession = session;

			if (defaultInit)
			{
				mStreamData = new BpStreamData();
				mStreamData.mBuffer = new List<uint8>();
				mStreamDataList.Add(mStreamData);

				// Idx 0 is the 'overview'
				var streamLOD = new BpStreamLOD();
				mStreamLODs.Add(streamLOD);

				streamLOD = new BpStreamLOD();
				mStreamLODs.Add(streamLOD);
			}
		}
		
		public void SplitStreamData()
		{
			var prevStreamData = mStreamData;
			prevStreamData.mSplitTick = prevStreamData.mEndTick;
			// Leave room to add the .Leave commands
			prevStreamData.mSizeOverride = prevStreamData.mBuffer.Count + (mEntryStack.Count * (1 + 8+1));

			int64 numTicks = mStreamData.mEndTick - mStreamData.mStartTick;
			int64 avgZoneDist = 0;
			if (mStreamData.mNumZones > 0)
				avgZoneDist = numTicks / mStreamData.mNumZones;
			let streamDataLOD0 = mStreamLODs[0].Generate(this, mStreamDataList, (int32)mStreamDataList.Count - 1, avgZoneDist * 10);

			numTicks = streamDataLOD0.mEndTick - streamDataLOD0.mStartTick;
			avgZoneDist = 0;
			if (streamDataLOD0.mNumZones > 0)
				avgZoneDist = numTicks / streamDataLOD0.mNumZones;
#unwarn
			let streamDataLOD1 = mStreamLODs[1].Generate(this, mStreamLODs[0].mStreamDataList, (int32)mStreamLODs[0].mStreamDataList.Count - 1, avgZoneDist * 10);

			mStreamData = new BpStreamData();
			mStreamDataList.Add(mStreamData);

			// We actually copy the buffer data to place in the old stream data and we keep around the 'scratch buffer'
			//  for the newest stream data.  This is to avoid keeping around unused bytes in a closed buffer
			var oldBuffer = new List<uint8>();
			oldBuffer.Capacity = (int32)prevStreamData.mSizeOverride;
			prevStreamData.mBuffer.CopyTo(oldBuffer);
			mStreamData.mBuffer = prevStreamData.mBuffer;
			mStreamData.mBuffer.Clear();
			prevStreamData.mBuffer = oldBuffer;
			prevStreamData.mPendingLeaveDepth = (int32)mEntryStack.Count;

			mStreamData.mStartTick = prevStreamData.mEndTick;
			mStreamData.mEndTick = prevStreamData.mEndTick;

			if (mEntryStack.Count > 0)
			{
				mStreamData.Write((uint8)BpCmd.StreamSplitInfo);
				mStreamData.WriteSLEB128(mEntryStack.Count);
			}	

			for (var entry in ref mEntryStack)
			{
				mStreamData.Write((uint8)BpCmd.Enter);
				mStreamData.WriteTickDelta(entry.mStartTick);
				mStreamData.WriteSLEB128(entry.mZoneNameId);

				int32 newParamsPos = (int32)mStreamData.mBuffer.Count;

				int32 paramSize = 0;
				if (entry.mZoneNameId < 0)
				{
					paramSize = -1;
					int32 nameLen = -entry.mZoneNameId;
					mStreamData.Write(&prevStreamData.mBuffer[entry.mParamsReadPos - nameLen], nameLen);
				}
				else
				{
					var zoneName = mSession.mZoneNames[entry.mZoneNameId];
					paramSize = zoneName.mParamsSize;
				}
				
				if (paramSize != 0)
				{
					BPStateContext stateCtx = scope BPStateContext(mSession, prevStreamData);
					stateCtx.ReadPos = entry.mParamsReadPos;
					if (paramSize == -1)
					{
						paramSize = (int32)stateCtx.ReadSLEB128();
						mStreamData.WriteSLEB128(paramSize);
					}
					if (paramSize > 0)
					{
						int32 prevSize = (int32)mStreamData.mBuffer.Count;
						mStreamData.mBuffer.GrowUnitialized(paramSize);
						stateCtx.Read(&mStreamData.mBuffer[prevSize], paramSize);
					}
					entry.mParamsReadPos = newParamsPos;
				}
			}

			if (mLastFrameTick != 0)
			{
				mStreamData.Write((uint8)BpCmd.PrevFrameTick);
				mStreamData.WriteTickDelta(mLastFrameTick);
			}
		}
		
		public void MaybeSplitStreamData()
		{
			if (mStreamData.mBuffer.Count >= 64*1024)
			{
				SplitStreamData();

				/*if (mStreamDataList.Count == 2)
				{
					Debug.WriteLine("Split Tick: {0}, BufferSize={1}", mStreamDataList[0].mSplitTick, mStreamDataList[0].mBuffer.Count);
					for (var entry in mEntryStack)
					{
						Debug.WriteLine("mEntryStack[{0}] mStartTick={1}", @entry.Index, entry.mStartTick);
					}
				}*/
			}
		}
	}

	class BpTrack : BpCmdTarget
	{
		public String mName ~ delete _;

		public this(BpSession session) : base(session, false)
		{

		}

		public this(BpSession session, int32 nativeThreadId) : base(session, true)
		{
			mNativeThreadId = nativeThreadId;
		}
		
		public void GetName(String str)
		{
			if (mName != null)
				str.Append(mName);
			else
				str.AppendF("Thread {0}", mNativeThreadId);
		}

		public static int Compare(BpTrack lhs, BpTrack rhs)
		{
			String lhsName = lhs.mName;
			String rhsName = rhs.mName;
			if ((lhsName == null) && (rhsName == null))
				return lhs.mNativeThreadId - rhs.mNativeThreadId;
			if ((lhsName != null) && (rhsName != null))
				return String.Compare(lhsName, rhsName, true);
			if (lhsName == null)
				return String.Compare("Thread ", rhsName, true);
			return String.Compare(lhsName, "Thread ", true);
		}
	}

	class BpZoneName
	{
		public String mName ~ delete _;
		public int32 mParamsSize; // -1 = variable sized (string)
	}

	class BpSession 
	{
		public enum LoadError
		{
			FileNotFound,
			FileError,
			InvalidData,
			InvalidVersion
		}

		public Stream mStream ~ delete _;

		public const int cBufTickScale = 100;
		public const uint32 cFileMarker = 0xBEEF193A;

		public BpCmdTarget mCurCmdTarget;
		public DateTime mConnectTime;
		public DateTime mDisconnectTime;
		public int64 mNumZones;
		public String mClientEnv ~ delete _;
		public String mClientName ~ delete _;
		public String mSessionId ~ delete _;
		public String mSessionName ~ delete _;

		public List<BpTrack> mThreads = new List<BpTrack>() ~ DeleteContainerAndItems!(_);
		public BpCmdTarget mRootCmdTarget ~ delete _;
		public List<BpZoneName> mZoneNames = new List<BpZoneName>() ~ DeleteContainerAndItems!(_);
		public int64 mStatTotalBytesReceived;
		public int64 mFirstTick;
		public int64 mCurTick;

		public int32 mThreadDataVersion;
		public int32 mDispBPS;
		public bool mSessionOver;
		public double mTicksToUSScale;

		/*public int64 mFirstTimeStamp = -1;
		public uint32 mFirstTickMS;*/

		private this()
		{

		}

		public this(bool defaultInit)
		{
			mRootCmdTarget = new BpCmdTarget(this, defaultInit);
		}

		protected void ProcessClientEnv()
		{
			for (var line in mClientEnv.Split('\n'))
			{
				var lineEnum = line.Split('\t');
				if ((StringView key = lineEnum.GetNext()) &&
					(StringView value = lineEnum.GetNext()))
				{
					switch (key)
					{
					case "ClientName": mClientName = new String(value);
					case "SessionId": mSessionId = new String(value);
					case "SessionName": mSessionName = new String(value);
					}
				}
			}
		}

		public double GetTicksToUSScale()
		{
			return mTicksToUSScale;
		}

		public double TicksToUS(int64 ticks)
		{
			return ticks * mTicksToUSScale;
		}

		public void CalcBPS()
		{
			TimeSpan elapsed;
			if (mSessionOver)
				elapsed = mDisconnectTime - mConnectTime;
			else
				elapsed = DateTime.Now - mConnectTime;

			mDispBPS = 0;
			if (elapsed.Milliseconds > 0)
				mDispBPS = (int32)(mStatTotalBytesReceived * 1000 / elapsed.TotalMilliseconds / 1024);
		}

		public void ElapsedTicksToStr(int64 ticks, String str)
		{
			double timeUS = (double)(ticks * GetTicksToUSScale());
			ElapsedTimeToStr(timeUS, str);
		}

		public Result<int64> ParseElapsedTicks(String str)
		{
			double timeUS = 0;

			int numStart = -1;
			int numEnd = -1;
			double curNumber = 0;
			char8 prevChar = 0;
			bool hasNumber = false;
			for (int i < str.Length)
			{
				char8 c = str[i];

				if (((c >= '0') && (c <= '9')) || (c == '.'))
				{
					if (numStart == -1)
						numStart = i;
					numEnd = i;
					hasNumber = true;
					continue;
				}

				if (hasNumber)
				{
					if (float.Parse(scope String(str, numStart, numEnd - numStart + 1)) case .Ok(let num))
						curNumber = num;
					else
						return .Err;
					numStart = -1;
					numEnd = -1;
					hasNumber = false;
				}

				if (c.IsWhiteSpace)
					continue;
				if (c == ':')
					continue;

				if ((c == 'm') || (c == 'u'))
				{
					if (prevChar != 0)
						return .Err;
					prevChar = c;
				}
				else if (c == 's')
				{
					double secScale;
					if (prevChar == 'u')
						secScale = 1.0;
					else if (prevChar == 'm')
						secScale = 0.001;
					else
						secScale = 0.000001;
					timeUS += curNumber * secScale;
					hasNumber = false;
				}
				else // Invalid char
					return .Err;
			}

			// Shouldn't have a leftover non-terminated number
			if (hasNumber)
				return .Err;

			return .Ok((int64)(timeUS / GetTicksToUSScale()));
		}

		public static void ElapsedTimeToStr(double timeUS, String str)
		{
			int64 intTimeUS = (int64)timeUS;

			if (intTimeUS < 1000) // < 1ms
			{
				int32 timeNS = (int32)((timeUS - intTimeUS) * 1000);
				str.AppendF("{0}.{1:02}us", intTimeUS, timeNS / 10);
			}
			else if (intTimeUS < 1000*1000) // < 1s
			{
				str.AppendF("{0}.{1:02}ms", (int32)(intTimeUS / 1000), (int32)((intTimeUS / 10) % 100));
			}
			else if (intTimeUS < 10*1000*1000) // < 10s
			{
				str.AppendF("{0}s:{1:03}.{2:01}ms", (int32)(intTimeUS / 1000 / 1000), (int32)((intTimeUS / 1000) % 1000), (int32)((intTimeUS / 100) % 10));
			}
			else if (intTimeUS < 60*1000*1000) // < 1m
			{
				str.AppendF("{0}s:{1:03}ms", (int32)(intTimeUS / 1000 / 1000), (int32)((intTimeUS / 1000) % 1000));
			}
			else
			{
				str.AppendF("{0}m:{1}s:{2}ms", (int32)(intTimeUS / 60 / 60 / 1000 / 1000), (int32)((intTimeUS / 60 / 1000 / 1000) % 60), (int32)((intTimeUS / 1000) % 60));
			}
		}

		public static void TimeToStr(double timeUS, String str, bool highPrecision = true)
		{
			int64 intTimeUS = (int64)timeUS;
			str.AppendF("{0}:{1:02}:", (int32)((intTimeUS / 60 / 60 / 1000000)), (int32)((intTimeUS / 60 / 1000000) % 60));
			if (highPrecision)
				str.AppendF("{0:02}.{1:06}", (int32)((intTimeUS / 1000000) % 60), (int32)((intTimeUS % 1000000)));
			else
				str.AppendF("{0:02}.{1:02}", (int32)((intTimeUS / 1000000) % 60), (int32)((intTimeUS / 10000 % 100)));
		}

		public Result<int64> ParseTime(String str)
		{
			double timeUS = 0;

			int numStart = -1;
			int numEnd = -1;
			double curNumber = 0;
			bool hasParsedNumber = false;
			bool hasNumberChars = false;

			List<double> sections = scope List<double>(4);

			for (int i < str.Length)
			{
				char8 c = str[i];
				
				bool ending = false;
				if (((c >= '0') && (c <= '9')) || (c == '.'))
				{
					if (hasParsedNumber)
						return .Err;
					if (numStart == -1)
						numStart = i;
					numEnd = i;
					hasNumberChars = true;

					if (i < str.Length - 1)
						continue;
					ending = true;
				}

				if (hasNumberChars)
				{
					if (double.Parse(scope String(str, numStart, numEnd - numStart + 1)) case .Ok(let num))
						curNumber = num;
					else
						return .Err;
					numStart = -1;
					numEnd = -1;
					hasNumberChars = false;
					hasParsedNumber = true;
				}

				if (ending)
					break;

				if (c.IsWhiteSpace)
					continue;
				if (c == ':')
				{
					if (hasParsedNumber)
					{
						sections.Add(curNumber);
						hasParsedNumber = false;
					}

					continue;
				}
				else
					return .Err;
			}

			double seconds = curNumber;
			if (sections.Count == 2)
			{
				seconds += sections[0]*60*60 + sections[1]*60;
			}
			else
				return .Err;

			timeUS += seconds * 1000000.0;

			return .Ok((int64)(timeUS / GetTicksToUSScale()) + mFirstTick);
		}

		public void TicksToStr(int64 tick, String str)
		{
			TimeToStr((tick - mFirstTick) * GetTicksToUSScale(), str);
		}

		public void ClearData()
		{
			mFirstTick = mCurTick;
			mNumZones = 0;
			ClearCommandTarget(mRootCmdTarget);
			for (var thread in mThreads)
				ClearCommandTarget(thread);
		}

		void ClearCommandTarget(BpCmdTarget cmdTarget)
		{
			cmdTarget.SplitStreamData(); // Start a new, clear streamData
			while (cmdTarget.mStreamDataList.Count > 1)
			{
				var secondToLastIdx = cmdTarget.mStreamDataList.Count - 2;
				delete cmdTarget.mStreamDataList[secondToLastIdx];
				cmdTarget.mStreamDataList.RemoveAt(secondToLastIdx);
			}
			for (var lod in cmdTarget.mStreamLODs)
			{
				ClearAndDeleteItems(lod.mStreamDataList);
			}
			cmdTarget.mEntryStack.Clear();
		}

		public Result<void> Save(StringView filePath)
		{
			var fs = scope FileStream();

			void SaveStreamData(BpStreamData streamData)
			{
				fs.Write((int32)streamData.mBuffer.Count);
				fs.Write((Span<uint8>)streamData.mBuffer);

				fs.Write(streamData.mStartTick);
				fs.Write(streamData.mEndTick);
				fs.Write(streamData.mSplitTick);
				fs.Write(streamData.mSizeOverride);
				fs.Write(streamData.mPendingLeaveDepth);
				fs.Write(streamData.mNumZones);
				fs.Write(streamData.mMinTicks);
			}

			void SaveCmdTarget(BpCmdTarget cmdTarget)
			{
				fs.Write(cmdTarget.mNativeThreadId);
				fs.Write(cmdTarget.mCreatedTick);
				fs.Write(cmdTarget.mRemoveTick);

				fs.Write((int32)cmdTarget.mStreamLODs.Count);
				for (var streamLod in cmdTarget.mStreamLODs)
				{
					fs.Write(streamLod.mMinTicks);
					fs.Write((int32)streamLod.mStreamDataList.Count);
					for (var streamData in streamLod.mStreamDataList)
						SaveStreamData(streamData);
				}

				fs.Write((int32)cmdTarget.mStreamDataList.Count);
				for (var streamData in cmdTarget.mStreamDataList)
					SaveStreamData(streamData);

				fs.Write((int32)cmdTarget.mEntryStack.Count);
				if (!cmdTarget.mEntryStack.IsEmpty)
					fs.Write(Span<uint8>((uint8*)&cmdTarget.mEntryStack[0], cmdTarget.mEntryStack.Count * sizeof(BPEntry)));

				fs.Write(cmdTarget.mLastFrameTick);
			}

			switch (fs.Create(filePath))
			{
			case .Ok:
			case .Err:
				return .Err;
			}

			fs.Write((int32)cFileMarker);
			fs.Write((int32)2); // Version

			fs.Write(mConnectTime.ToBinaryRaw());
			fs.Write(mDisconnectTime.ToBinaryRaw());
			fs.Write(mNumZones);
			fs.WriteStrSized32(mClientEnv);

			fs.Write((int32)mThreads.Count);
			for (var thread in mThreads)
			{
				fs.WriteStrSized32(thread.mName ?? "");
				SaveCmdTarget(thread);
			}
			SaveCmdTarget(mRootCmdTarget);
			fs.Write((int32)mZoneNames.Count);
			for (var cmdName in mZoneNames)
			{
				fs.WriteStrSized32(cmdName.mName);
				fs.Write(cmdName.mParamsSize);
			}
			fs.Write(mStatTotalBytesReceived);
			fs.Write(mFirstTick);
			fs.Write(mCurTick);
			fs.Write(mClientEnv);

			return .Ok;
		}

		public Result<void, LoadError> Load(StringView filePath)
		{
			var fs = scope FileStream();
			bool readFailed = false;

			switch (fs.Open(filePath, .Read))
			{
			case .Ok:
			case .Err(let err):
				switch (err)
				{
				case .NotFound:
					return .Err(.FileNotFound);
				default:
					return .Err(.FileError);
				}
			}

			T Read<T>() where T : struct
			{
				switch (fs.Read<T>())
				{
				case .Ok(let setVal):
					return setVal;
				case .Err:
					readFailed = true;
				}
				return default;
			}

			void Read<T>(out T val) where T : struct
			{
				switch (fs.Read<T>())
				{
				case .Ok(let setVal):
					val = setVal;
					return;
				case .Err:
					val = default;
					readFailed = true;
				}
			}

			void ReadStrSized32(String s)
			{
				switch (fs.Read<int32>())
				{
				case .Ok(let len):
					char8* ptr = s.PrepareBuffer(len);
					switch (fs.TryRead(Span<uint8>((uint8*)ptr, len)))
					{
					case .Ok:
					case .Err:
						readFailed = true;
						return;
					}
				case .Err:
					readFailed = true;
				}
			}

			mixin LoadTry(var val)
			{
				if (val case .Err(var err))
					return .Err(.InvalidData);
				val.Get()
			}

			void Read(Span<uint8> data)
			{
				if (fs.TryRead(data) case .Err)
					readFailed = true;
			}

			void ReadStreamData(BpStreamData streamData)
			{
				int dataSize = Read<int32>();
				streamData.mBuffer = new List<uint8>();
				streamData.mBuffer.GrowUnitialized(dataSize);
				Read(streamData.mBuffer);

				Read(out streamData.mStartTick);
				Read(out streamData.mEndTick);
				Read(out streamData.mSplitTick);
				Read(out streamData.mSizeOverride);
				Read(out streamData.mPendingLeaveDepth);
				Read(out streamData.mNumZones);
				Read(out streamData.mMinTicks);
			}

			void ReadCmdTarget(BpCmdTarget cmdTarget)
			{
				Read(out cmdTarget.mNativeThreadId);
				Read(out cmdTarget.mCreatedTick);
				Read(out cmdTarget.mRemoveTick);

				int lodCount = Read<int32>();
				for (int lodIdx < lodCount)
				{
					var streamLod = new BpStreamLOD();
					cmdTarget.mStreamLODs.Add(streamLod);
					Read(out streamLod.mMinTicks);
					int dataCount = Read<int32>();
					for (int dataIdx < dataCount)
					{
						var streamData = new BpStreamData();
						ReadStreamData(streamData);
						streamLod.mStreamDataList.Add(streamData);
					}
				}

				int dataCount = Read<int32>();
				for (int dataIdx < dataCount)
				{
					var streamData = new BpStreamData();
					ReadStreamData(streamData);
					cmdTarget.mStreamDataList.Add(streamData);
				}

				int stackSize = Read<int32>();
				if (stackSize > 0)
				{
					cmdTarget.mEntryStack.GrowUnitialized(stackSize);
					Read(Span<uint8>((uint8*)&cmdTarget.mEntryStack[0], cmdTarget.mEntryStack.Count * sizeof(BPEntry)));
				}	
				
				Read(out cmdTarget.mLastFrameTick);
			}

			uint32 fileMarker = Read<uint32>();
			if (fileMarker != cFileMarker)
				return .Err(.InvalidData);
			int32 version = Read<int32>();
			if ((version < 1) || (version > 2))
				return .Err(.InvalidVersion);
			mConnectTime = LoadTry!(DateTime.FromBinaryRaw(Read<int64>()));
			mDisconnectTime = LoadTry!(DateTime.FromBinaryRaw(Read<int64>()));
			Read(out mNumZones);
			mClientEnv = new String();
			ReadStrSized32(mClientEnv);
			ProcessClientEnv();

			int threadCount = Read<int32>();
			for (int threadIdx < threadCount)
			{
				var thread = new BpTrack(this);
				thread.mName = new String();
				ReadStrSized32(thread.mName);
				if (thread.mName.IsEmpty)
				{
					DeleteAndNullify!(thread.mName);
				}
				ReadCmdTarget(thread);
				mThreads.Add(thread);
			}
			ReadCmdTarget(mRootCmdTarget);
			int zoneCount = Read<int32>();
			for (int zoneIdx < zoneCount)
			{
				var zoneName = new BpZoneName();
				zoneName.mName = new String();
				ReadStrSized32(zoneName.mName);
				zoneName.mParamsSize = Read<int32>();
				mZoneNames.Add(zoneName);
			}
			Read(out mStatTotalBytesReceived);
			Read(out mFirstTick);
			Read(out mCurTick);
			if (version > 1)
				Read(out mTicksToUSScale);
			else
				mTicksToUSScale = 1 / 4500.0;

			if (fs.Position != fs.Length)
				return .Err(.InvalidData);

			if (readFailed)
				return .Err(.InvalidData);

			mThreadDataVersion = 1;
			mSessionOver = true;
			CalcBPS();

			return .Ok;
		}
	}

	class BpClient : BpSession
	{
		public Socket mSocket ~ delete _;
		public Monitor mDataMonitor = new Monitor() ~ delete _;
		public CircularBuffer mCircularBuffer = new CircularBuffer() ~ delete _;
		public int32 mBlockRecvLeft;
		public int32 mCurBlockSize;
		public bool mGettingBlockSize = true;
		public ProfileInstance mProfileId;
		public int32 mValidDataSize;
		
		public int32 mUpdateCnt;
		public bool mSessionInitialized;

		public this() : base(true)
		{
			//mCircularBuffer.Resize(128*1024*1024);

			mCurCmdTarget = mRootCmdTarget;
		}

		public ~this()
		{
			if (mProfileId != 0)
				mProfileId.Dispose();
		}

		public bool IsSessionInitialized
		{
			get
			{
				return mSessionInitialized;
			}
		}

		public void TryRecv()
		{
			if (!mSocket.IsOpen)
			{
				return;
			}

			if (mSessionOver)
			{
				if (mProfileId != 0)
				{
					mProfileId.Dispose();
					mProfileId = 0;
				}
				mSocket.Close();
				return;
			}

			let recvSize = 0x10000;
			uint8* data = scope uint8[recvSize]*;

			while (true)												   
			{
				if (mGettingBlockSize)
				{
					int32 wantsRecvSize;
					using (mDataMonitor.Enter())
					{
						int32 extraData = mCircularBuffer.GetSize() - mValidDataSize;
						wantsRecvSize = 4 - extraData;
					}
					int32 result = -1;
					if (mSocket.Recv(data, wantsRecvSize) case .Ok(let recvBytes))
					{
						result = (int32)recvBytes;
					}
					else
					{
						if (!mSocket.IsConnected)
						{
							Disconnect();
						}
					}
					using (mDataMonitor.Enter())
					{
						if (result > 0)
						{
							int32 pos = mCircularBuffer.GetSize();
							mCircularBuffer.Grow(result);
							mCircularBuffer.Write(data, pos, result);
							gApp.ReportBytesReceived(result);
							mStatTotalBytesReceived += result;
						}
	
						if (result == wantsRecvSize)
						{
							mCircularBuffer.Read(&mCurBlockSize, mValidDataSize, 4);
							mBlockRecvLeft = mCurBlockSize;
							mCircularBuffer.RemoveBack(4);

#if LINEAR_OPT
							mCircularBuffer.EnsureSpan(mBlockRecvLeft);
#endif							
							mGettingBlockSize = false;

							//Debug.WriteLine("Recv block size {0}", mCurBlockSize);
							Debug.Assert(mCurBlockSize < 32*1024*1024); // Just ensure it's reasonable
						}
					}
				}
	
				if (mBlockRecvLeft <= 0)
					break;

				//Thread.Sleep(1);

				int32 tryRecvSize = (int32)Math.Min(mBlockRecvLeft, recvSize);
				int32 result = -1;
				if (mSocket.Recv(data, tryRecvSize) case .Ok(let recvBytes))
				{
					result = (int32)recvBytes;
				}
				if (result <= 0)
				{
					if (!mSocket.IsConnected)
					{
						Disconnect();
					}
					break;
				}
				
				gApp.ReportBytesReceived(result);
				using (mDataMonitor.Enter())
				{
					mStatTotalBytesReceived += result;
					int32 curSize = mCircularBuffer.GetSize();
					mCircularBuffer.Grow(result);
					mCircularBuffer.Write(data, curSize, result);
					mBlockRecvLeft -= result;
					if (mBlockRecvLeft == 0)
					{
	                    mValidDataSize += mCurBlockSize;
						mCurBlockSize = 0;
						mGettingBlockSize = true;
					}
				}
			}
		}

		void ReadStr(String str)
		{
			while (true)
			{
				char8 c = (char8)mCircularBuffer.Read();
				if (c == 0)
					return;
				str.Append(c);
			}
		}

		int mReadIdx = 0;
		int mFrameTickCount = 0;

		void ReadTickDelta()
		{
			int64 tickDelta = mCircularBuffer.ReadSLEB128() * cBufTickScale;
			mCurTick += tickDelta;
		}

		void Disconnect()
		{
			if (mSessionOver)
				return;

			mDisconnectTime = DateTime.Now;
			mSessionOver = true;
			CalcBPS();
		}

		void PopCurEntry()
		{
			mCurCmdTarget.mStreamData.Write((uint8)BpCmd.Leave);
			mCurCmdTarget.mStreamData.WriteTickDelta(mCurTick);

			var entry = mCurCmdTarget.mEntryStack.PopBack();
			int32 stackDepth = (int32)mCurCmdTarget.mEntryStack.Count;

			if (entry.mStartTick <= mCurCmdTarget.mStreamData.mStartTick)
			{
				// This spans back
				for (int checkStreamIdx = mCurCmdTarget.mStreamDataList.Count - 2; checkStreamIdx >= 0; checkStreamIdx--)
				{
					var streamData = mCurCmdTarget.mStreamDataList[checkStreamIdx];
					if (entry.mStartTick > streamData.mSplitTick)
						break; // Not applicable this far back

					// Is this the Leave we've been waiting for?
					if (streamData.mPendingLeaveDepth != stackDepth + 1)
						continue;
					streamData.mPendingLeaveDepth--;

					/*if (checkStreamIdx == 0)
					{
						Debug.WriteLine("Back-propagating Leaving {0} -> {1}", entry.mStartTick, mCurTick);
					}*/

					streamData.Write((uint8)BpCmd.Leave);
					streamData.WriteTickDelta(mCurTick);
					Debug.Assert(streamData.mBuffer.Count <= streamData.mSizeOverride);

					for (var lod in mCurCmdTarget.mStreamLODs)
					{
						if (checkStreamIdx < lod.mStreamDataList.Count)
						{
							var lodStreamData = lod.mStreamDataList[checkStreamIdx];
							if (lodStreamData != null)
							{
								lodStreamData.Write((uint8)BpCmd.Leave);
								lodStreamData.WriteTickDelta(mCurTick);
								Debug.Assert(lodStreamData.mBuffer.Count <= lodStreamData.mSizeOverride);
							}
						}
					}
				}
			}

			if ((mCurCmdTarget.mEntryStack.Count == 1) && (mCurCmdTarget.mStreamDataList.Count <= 2))
			{
				//cmdTarget.SplitStreamData();
			}

		}

		void ProcessData()
		{
			if (mSessionOver)
				return;

			Debug.Assert(mValidDataSize <= mCircularBuffer.GetSize());

			String tempStr = scope String(64);

			int readIdx = 0;
			
			BpCmd prevCmd = default;

			//return;

			while (mValidDataSize > 0)
			{
				mReadIdx++;
				readIdx++;

				int32 startSize = mCircularBuffer.GetSize();
				uint8* curPtr = mCircularBuffer.GetPtr();

				uint8 Read()
				{
					return *(curPtr++);
				}

				void ReadStr(String str)
				{
					uint8* ptr = curPtr;
					while (*ptr != 0)
					{
						ptr++;
					}

					str.Append((char8*)curPtr, ptr - curPtr);
					curPtr = ptr + 1;
				}

				int64 ReadSLEB128()
				{
					int64 value = 0;
					int32 shift = 0;
					int64 curByte;
					repeat
					{
						curByte = *(curPtr++);
						value |= ((curByte & 0x7f) << shift);
						shift += 7;
					
					} while (curByte >= 128);
					// Sign extend negative numbers.
					if (((curByte & 0x40) != 0) && (shift < 64))
						value |= ~0L << shift;
					return value;
				}

				void ReadTickDelta()
				{
					int64 tickDelta = ReadSLEB128() * cBufTickScale;
					mCurTick += tickDelta;
				}

				BpCmd cmd = (BpCmd)Read();
				switch (cmd)
				{
				case .Init:
					int32 version = (int32)ReadSLEB128();
					if (version < 2)
					{
						Disconnect();
						return;
					}
					Debug.Assert(version == 2);
					Debug.Assert(mClientName == null);
					mClientEnv = new String();
					ReadStr(mClientEnv);
					ProcessClientEnv();

					if (!gApp.mWorkspacePanel.PassesFilter(this))
					{
						Disconnect();
						break;
					}

					mSessionInitialized = true;
					gApp.SessionInitialized(this);
				case .StrEntry:
					ReadStr(tempStr);
					BpZoneName zoneName = new BpZoneName();
					zoneName.mName = new String(tempStr);
					bool isDyn = false;
					for (int idx = 0; idx < zoneName.mName.Length; idx++)
					{
						let c = zoneName.mName[idx];
						if (c == '%')
						{
							let cNext = zoneName.mName[idx + 1];
							idx++;

							if (cNext == 'd')
								zoneName.mParamsSize += 4;
							else if (cNext == 'f')
								zoneName.mParamsSize += 4;
							else if (cNext == 's')
								isDyn = true;
						}
					}
					if (isDyn)
						zoneName.mParamsSize = -1;
					mZoneNames.Add(zoneName);
					tempStr.Clear();
				case .Enter:
					mNumZones++;
					ReadTickDelta();
					int32 zoneNameId = (int32)ReadSLEB128();

					BPEntry entry;
					entry.mStartTick = mCurTick;
					entry.mZoneNameId = zoneNameId;

					int32 paramSize = 0;
					if (zoneNameId >= 0)
					{
						let zoneName = mZoneNames[zoneNameId];
						paramSize = zoneName.mParamsSize;
						/*if (paramSize != 0)
							entry.mParamsReadPos = cmdTarget.mStreamData.mBuffer.Count;
						else
							entry.mParamsReadPos = 0;*/
					}
					else
					{
						paramSize = -1;
						//entry.mParamsReadPos = cmdTarget.mStreamData.mBuffer.Count;
					}

					mCurCmdTarget.mStreamData.mNumZones++;
					mCurCmdTarget.mStreamData.Write((uint8)cmd);
					mCurCmdTarget.mStreamData.WriteTickDelta(mCurTick);
					mCurCmdTarget.mStreamData.WriteSLEB128(zoneNameId);
					if (zoneNameId < 0)
					{
						int32 strLen = -zoneNameId;
						//var view = scope CircularBuffer.View();
						/*mCircularBuffer.MapView(0, strLen, view);
						mCurCmdTarget.mStreamData.Write(view.mPtr, strLen);
						mCircularBuffer.RemoveFront(strLen);*/
						mCurCmdTarget.mStreamData.Write(curPtr, strLen);
						curPtr += strLen;
					}

					entry.mParamsReadPos = (int32)mCurCmdTarget.mStreamData.mBuffer.Count;
					mCurCmdTarget.mEntryStack.Add(entry);

					if (paramSize == -1)
					{
						paramSize = (int32)ReadSLEB128();
						mCurCmdTarget.mStreamData.WriteSLEB128(paramSize);
					}

					if (paramSize != 0)
					{
						/*var view = scope CircularBuffer.View();
						mCircularBuffer.MapView(0, paramSize, view);
						mCurCmdTarget.mStreamData.Write(view.mPtr, paramSize);
						mCircularBuffer.RemoveFront(paramSize);*/
						mCurCmdTarget.mStreamData.Write(curPtr, paramSize);
						curPtr += paramSize;
					}
					
				case .Leave:
					ReadTickDelta();
					
					// This can happen when we Init after some entries are already in progress
					if (mCurCmdTarget.mEntryStack.Count == 0)
						break;

					PopCurEntry();
				case .SetThread:
					int64 threadNum = ReadSLEB128();
					if (threadNum == -1)
						mCurCmdTarget = mRootCmdTarget;
					else
						mCurCmdTarget = mThreads[(int)threadNum];
				case .ThreadAdd:
					ReadTickDelta();
					int32 threadId = (int32)ReadSLEB128();
					int32 nativeThreadId = (int32)ReadSLEB128();
					BpTrack newThread = new BpTrack(this, threadId);
					newThread.mCreatedTick = mCurTick;
					newThread.mStreamData.mStartTick = mCurTick;
					newThread.mStreamData.mEndTick = mCurTick;
					newThread.mNativeThreadId = nativeThreadId;
					while (threadId >= mThreads.Count)
						mThreads.Add(null);
					mThreads[threadId] = newThread;
					mThreadDataVersion++;
				case .ThreadRemove:
					ReadTickDelta();
					mCurCmdTarget.mRemoveTick = mCurTick;
					while (mCurCmdTarget.mEntryStack.Count > 0)
						PopCurEntry();
				case .ThreadName:
					var thread = (BpTrack)mCurCmdTarget;
					if (thread.mName == null)
						thread.mName = new String();
					ReadStr(thread.mName);
					mThreadDataVersion++;
				case .FrameTick:
					ReadTickDelta();
					mCurCmdTarget.mStreamData.Write((uint8)cmd);
					mCurCmdTarget.mStreamData.WriteTickDelta(mCurTick);
					mCurCmdTarget.mLastFrameTick = mCurTick;

					mFrameTickCount++;
				case .KeepAlive:
					ReadTickDelta();
				case .ClockInfo:
					int64 curTick = ReadSLEB128() * 100;
					Debug.Assert(curTick == mCurTick);
#unwarn
					int64 timeStampNow = ReadSLEB128() * 100;
#unwarn
					uint32 tickMSNow = (.)ReadSLEB128();
					int64 clockRate = (.)ReadSLEB128();

					mTicksToUSScale = 1000000.0 / clockRate;

					/*if (mFirstTimeStamp == -1)
					{
						mFirstTimeStamp = timeStampNow;
						mFirstTickMS = tickMSNow;
					}
					else
					{
						int64 deltaMS = (.)(tickMSNow - mFirstTickMS);
						int64 deltaTimeStamp = timeStampNow - mFirstTimeStamp;
						if (deltaMS > 100)
						{

						}
					}*/

				case .Event:
					ReadTickDelta();
					var nameStr = scope String(64);
					var detailsStr = scope String(256);
					ReadStr(nameStr);
					ReadStr(detailsStr);

					mCurCmdTarget.mStreamData.Write((uint8)cmd);
					mCurCmdTarget.mStreamData.WriteTickDelta(mCurTick);
					mCurCmdTarget.mStreamData.Write(nameStr.CStr(), nameStr.Length + 1);
					mCurCmdTarget.mStreamData.Write(detailsStr.CStr(), detailsStr.Length + 1);
				case .Cmd:
					var cmdStr = scope String();
					ReadStr(cmdStr);
					bool success = gApp.HandleClientCommand(this, cmdStr);
					int result = success ? 1 : 0;
					mSocket.Send(&result, 1).IgnoreError();
				default:
					Runtime.FatalError("Invalid stream byte");
				}
				prevCmd = cmd;

				mCircularBuffer.SetPtr(curPtr);
				
				if (mCurCmdTarget != null)
					mCurCmdTarget.MaybeSplitStreamData();

				if ((mFirstTick == 0) && (mCurTick != 0))
					mFirstTick = mCurTick;

				mValidDataSize -= (startSize - mCircularBuffer.GetSize());
			}
		}

		static int64 sDbgTick = 0;

		public void Update()
		{
			mUpdateCnt++;

			using (mDataMonitor.Enter())
			{
				ProcessData();
			}

			if (gApp.mUpdateCnt % 20 == 0)
				CalcBPS();
		}
	}
}
