Merge commit '619c39e5144f15aca129d6d999bcc5c34133ee64' into careminster

Conflicts:
	OpenSim/Region/ClientStack/Linden/Caps/GetMeshModule.cs
avinationmerge
Melanie 2012-11-20 01:20:59 +00:00
commit ccc81183b0
4 changed files with 547 additions and 79 deletions

View File

@ -45,16 +45,53 @@ namespace OpenSim.Capabilities.Handlers
{ {
public class GetMeshHandler public class GetMeshHandler
{ {
// private static readonly ILog m_log = private static readonly ILog m_log =
// LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private IAssetService m_assetService; private IAssetService m_assetService;
public const string DefaultFormat = "vnd.ll.mesh";
public GetMeshHandler(IAssetService assService) public GetMeshHandler(IAssetService assService)
{ {
m_assetService = assService; m_assetService = assService;
} }
public Hashtable Handle(Hashtable request)
{
Hashtable ret = new Hashtable();
ret["int_response_code"] = (int)System.Net.HttpStatusCode.NotFound;
ret["content_type"] = "text/plain";
ret["keepalive"] = false;
ret["reusecontext"] = false;
ret["int_bytes"] = 0;
string MeshStr = (string)request["mesh_id"];
//m_log.DebugFormat("[GETMESH]: called {0}", MeshStr);
if (m_assetService == null)
{
m_log.Error("[GETMESH]: Cannot fetch mesh " + MeshStr + " without an asset service");
}
UUID meshID;
if (!String.IsNullOrEmpty(MeshStr) && UUID.TryParse(MeshStr, out meshID))
{
// m_log.DebugFormat("[GETMESH]: Received request for mesh id {0}", meshID);
ret = ProcessGetMesh(request, UUID.Zero, null);
}
else
{
m_log.Warn("[GETMESH]: Failed to parse a mesh_id from GetMesh request: " + (string)request["uri"]);
}
return ret;
}
public Hashtable ProcessGetMesh(Hashtable request, UUID AgentId, Caps cap) public Hashtable ProcessGetMesh(Hashtable request, UUID AgentId, Caps cap)
{ {
Hashtable responsedata = new Hashtable(); Hashtable responsedata = new Hashtable();
@ -62,6 +99,7 @@ namespace OpenSim.Capabilities.Handlers
responsedata["content_type"] = "text/plain"; responsedata["content_type"] = "text/plain";
responsedata["keepalive"] = false; responsedata["keepalive"] = false;
responsedata["str_response_string"] = "Request wasn't what was expected"; responsedata["str_response_string"] = "Request wasn't what was expected";
responsedata["reusecontext"] = false;
string meshStr = string.Empty; string meshStr = string.Empty;
@ -77,6 +115,7 @@ namespace OpenSim.Capabilities.Handlers
responsedata["content_type"] = "text/plain"; responsedata["content_type"] = "text/plain";
responsedata["keepalive"] = false; responsedata["keepalive"] = false;
responsedata["str_response_string"] = "The asset service is unavailable. So is your mesh."; responsedata["str_response_string"] = "The asset service is unavailable. So is your mesh.";
responsedata["reusecontext"] = false;
return responsedata; return responsedata;
} }
@ -86,9 +125,83 @@ namespace OpenSim.Capabilities.Handlers
{ {
if (mesh.Type == (SByte)AssetType.Mesh) if (mesh.Type == (SByte)AssetType.Mesh)
{ {
responsedata["str_response_string"] = Convert.ToBase64String(mesh.Data);
responsedata["content_type"] = "application/vnd.ll.mesh"; Hashtable headers = new Hashtable();
responsedata["int_response_code"] = 200; responsedata["headers"] = headers;
string range = String.Empty;
if (((Hashtable)request["headers"])["range"] != null)
range = (string)((Hashtable)request["headers"])["range"];
else if (((Hashtable)request["headers"])["Range"] != null)
range = (string)((Hashtable)request["headers"])["Range"];
if (!String.IsNullOrEmpty(range)) // Mesh Asset LOD // Physics
{
// Range request
int start, end;
if (TryParseRange(range, out start, out end))
{
// Before clamping start make sure we can satisfy it in order to avoid
// sending back the last byte instead of an error status
if (start >= mesh.Data.Length)
{
responsedata["int_response_code"] = 404; //501; //410; //404;
responsedata["content_type"] = "text/plain";
responsedata["keepalive"] = false;
responsedata["str_response_string"] = "This range doesnt exist.";
responsedata["reusecontext"] = false;
return responsedata;
}
else
{
end = Utils.Clamp(end, 0, mesh.Data.Length - 1);
start = Utils.Clamp(start, 0, end);
int len = end - start + 1;
//m_log.Debug("Serving " + start + " to " + end + " of " + texture.Data.Length + " bytes for texture " + texture.ID);
if (start == 0 && len == mesh.Data.Length) // well redudante maybe
{
responsedata["int_response_code"] = (int) System.Net.HttpStatusCode.OK;
responsedata["bin_response_data"] = mesh.Data;
responsedata["int_bytes"] = mesh.Data.Length;
responsedata["reusecontext"] = false;
}
else
{
responsedata["int_response_code"] =
(int) System.Net.HttpStatusCode.PartialContent;
headers["Content-Range"] = String.Format("bytes {0}-{1}/{2}", start, end,
mesh.Data.Length);
byte[] d = new byte[len];
Array.Copy(mesh.Data, start, d, 0, len);
responsedata["bin_response_data"] = d;
responsedata["int_bytes"] = len;
responsedata["reusecontext"] = false;
}
}
}
else
{
m_log.Warn("[GETMESH]: Failed to parse a range from GetMesh request, sending full asset: " + (string)request["uri"]);
responsedata["str_response_string"] = Convert.ToBase64String(mesh.Data);
responsedata["content_type"] = "application/vnd.ll.mesh";
responsedata["int_response_code"] = 200;
responsedata["reusecontext"] = false;
}
}
else
{
responsedata["str_response_string"] = Convert.ToBase64String(mesh.Data);
responsedata["content_type"] = "application/vnd.ll.mesh";
responsedata["int_response_code"] = 200;
responsedata["reusecontext"] = false;
}
} }
// Optionally add additional mesh types here // Optionally add additional mesh types here
else else
@ -97,6 +210,7 @@ namespace OpenSim.Capabilities.Handlers
responsedata["content_type"] = "text/plain"; responsedata["content_type"] = "text/plain";
responsedata["keepalive"] = false; responsedata["keepalive"] = false;
responsedata["str_response_string"] = "Unfortunately, this asset isn't a mesh."; responsedata["str_response_string"] = "Unfortunately, this asset isn't a mesh.";
responsedata["reusecontext"] = false;
return responsedata; return responsedata;
} }
} }
@ -106,11 +220,27 @@ namespace OpenSim.Capabilities.Handlers
responsedata["content_type"] = "text/plain"; responsedata["content_type"] = "text/plain";
responsedata["keepalive"] = false; responsedata["keepalive"] = false;
responsedata["str_response_string"] = "Your Mesh wasn't found. Sorry!"; responsedata["str_response_string"] = "Your Mesh wasn't found. Sorry!";
responsedata["reusecontext"] = false;
return responsedata; return responsedata;
} }
} }
return responsedata; return responsedata;
} }
private bool TryParseRange(string header, out int start, out int end)
{
if (header.StartsWith("bytes="))
{
string[] rangeValues = header.Substring(6).Split('-');
if (rangeValues.Length == 2)
{
if (Int32.TryParse(rangeValues[0], out start) && Int32.TryParse(rangeValues[1], out end))
return true;
}
}
start = end = 0;
return false;
}
} }
} }

View File

@ -53,7 +53,8 @@ namespace OpenSim.Framework.Servers.HttpServer
Normal = 0, Normal = 0,
LslHttp = 1, LslHttp = 1,
Inventory = 2, Inventory = 2,
Texture = 3 Texture = 3,
Mesh = 4
} }
public PollServiceEventArgs( public PollServiceEventArgs(

View File

@ -27,11 +27,14 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reflection; using System.Reflection;
using System.IO; using System.IO;
using System.Threading;
using System.Web; using System.Web;
using Mono.Addins; using Mono.Addins;
using OpenSim.Framework.Monitoring;
using log4net; using log4net;
using Nini.Config; using Nini.Config;
using OpenMetaverse; using OpenMetaverse;
@ -57,9 +60,43 @@ namespace OpenSim.Region.ClientStack.Linden
private IAssetService m_AssetService; private IAssetService m_AssetService;
private bool m_Enabled = true; private bool m_Enabled = true;
private string m_URL; private string m_URL;
struct aPollRequest
{
public PollServiceMeshEventArgs thepoll;
public UUID reqID;
public Hashtable request;
}
public class aPollResponse
{
public Hashtable response;
public int bytes;
}
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static GetMeshHandler m_getMeshHandler;
private IAssetService m_assetService = null;
private Dictionary<UUID, string> m_capsDict = new Dictionary<UUID, string>();
private static Thread[] m_workerThreads = null;
private static OpenMetaverse.BlockingQueue<aPollRequest> m_queue =
new OpenMetaverse.BlockingQueue<aPollRequest>();
private Dictionary<UUID, PollServiceMeshEventArgs> m_pollservices = new Dictionary<UUID, PollServiceMeshEventArgs>();
#region Region Module interfaceBase Members #region Region Module interfaceBase Members
~GetMeshModule()
{
foreach (Thread t in m_workerThreads)
Watchdog.AbortThread(t.ManagedThreadId);
}
public Type ReplaceableInterface public Type ReplaceableInterface
{ {
get { return null; } get { return null; }
@ -83,6 +120,8 @@ namespace OpenSim.Region.ClientStack.Linden
return; return;
m_scene = pScene; m_scene = pScene;
m_assetService = pScene.AssetService;
} }
public void RemoveRegion(Scene scene) public void RemoveRegion(Scene scene)
@ -91,6 +130,9 @@ namespace OpenSim.Region.ClientStack.Linden
return; return;
m_scene.EventManager.OnRegisterCaps -= RegisterCaps; m_scene.EventManager.OnRegisterCaps -= RegisterCaps;
m_scene.EventManager.OnDeregisterCaps -= DeregisterCaps;
m_scene.EventManager.OnThrottleUpdate -= ThrottleUpdate;
m_scene = null; m_scene = null;
} }
@ -101,6 +143,27 @@ namespace OpenSim.Region.ClientStack.Linden
m_AssetService = m_scene.RequestModuleInterface<IAssetService>(); m_AssetService = m_scene.RequestModuleInterface<IAssetService>();
m_scene.EventManager.OnRegisterCaps += RegisterCaps; m_scene.EventManager.OnRegisterCaps += RegisterCaps;
// We'll reuse the same handler for all requests.
m_getMeshHandler = new GetMeshHandler(m_assetService);
m_scene.EventManager.OnDeregisterCaps += DeregisterCaps;
m_scene.EventManager.OnThrottleUpdate += ThrottleUpdate;
if (m_workerThreads == null)
{
m_workerThreads = new Thread[2];
for (uint i = 0; i < 2; i++)
{
m_workerThreads[i] = Watchdog.StartThread(DoMeshRequests,
String.Format("MeshWorkerThread{0}", i),
ThreadPriority.Normal,
false,
false,
null,
int.MaxValue);
}
}
} }
@ -110,25 +173,209 @@ namespace OpenSim.Region.ClientStack.Linden
#endregion #endregion
private void DoMeshRequests()
{
while (true)
{
aPollRequest poolreq = m_queue.Dequeue();
poolreq.thepoll.Process(poolreq);
}
}
// Now we know when the throttle is changed by the client in the case of a root agent or by a neighbor region in the case of a child agent.
public void ThrottleUpdate(ScenePresence p)
{
byte[] throttles = p.ControllingClient.GetThrottlesPacked(1);
UUID user = p.UUID;
int imagethrottle = ExtractTaskThrottle(throttles);
PollServiceMeshEventArgs args;
if (m_pollservices.TryGetValue(user, out args))
{
args.UpdateThrottle(imagethrottle);
}
}
private int ExtractTaskThrottle(byte[] pthrottles)
{
byte[] adjData;
int pos = 0;
if (!BitConverter.IsLittleEndian)
{
byte[] newData = new byte[7 * 4];
Buffer.BlockCopy(pthrottles, 0, newData, 0, 7 * 4);
for (int i = 0; i < 7; i++)
Array.Reverse(newData, i * 4, 4);
adjData = newData;
}
else
{
adjData = pthrottles;
}
// 0.125f converts from bits to bytes
//int resend = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
//pos += 4;
// int land = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
//pos += 4;
// int wind = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
// pos += 4;
// int cloud = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
// pos += 4;
pos += 16;
int task = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
// pos += 4;
//int texture = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); //pos += 4;
//int asset = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f);
return task;
}
private class PollServiceMeshEventArgs : PollServiceEventArgs
{
private List<Hashtable> requests =
new List<Hashtable>();
private Dictionary<UUID, aPollResponse> responses =
new Dictionary<UUID, aPollResponse>();
private Scene m_scene;
private CapsDataThrottler m_throttler = new CapsDataThrottler(100000, 1400000, 10000);
public PollServiceMeshEventArgs(UUID pId, Scene scene) :
base(null, null, null, null, pId, int.MaxValue)
{
m_scene = scene;
// x is request id, y is userid
HasEvents = (x, y) =>
{
lock (responses)
{
bool ret = m_throttler.hasEvents(x, responses);
m_throttler.ProcessTime();
return ret;
}
};
GetEvents = (x, y) =>
{
lock (responses)
{
try
{
return responses[x].response;
}
finally
{
responses.Remove(x);
}
}
};
// x is request id, y is request data hashtable
Request = (x, y) =>
{
aPollRequest reqinfo = new aPollRequest();
reqinfo.thepoll = this;
reqinfo.reqID = x;
reqinfo.request = y;
m_queue.Enqueue(reqinfo);
};
// this should never happen except possible on shutdown
NoEvents = (x, y) =>
{
/*
lock (requests)
{
Hashtable request = requests.Find(id => id["RequestID"].ToString() == x.ToString());
requests.Remove(request);
}
*/
Hashtable response = new Hashtable();
response["int_response_code"] = 500;
response["str_response_string"] = "Script timeout";
response["content_type"] = "text/plain";
response["keepalive"] = false;
response["reusecontext"] = false;
return response;
};
}
public void Process(aPollRequest requestinfo)
{
Hashtable response;
UUID requestID = requestinfo.reqID;
// If the avatar is gone, don't bother to get the texture
if (m_scene.GetScenePresence(Id) == null)
{
response = new Hashtable();
response["int_response_code"] = 500;
response["str_response_string"] = "Script timeout";
response["content_type"] = "text/plain";
response["keepalive"] = false;
response["reusecontext"] = false;
lock (responses)
responses[requestID] = new aPollResponse() { bytes = 0, response = response };
return;
}
response = m_getMeshHandler.Handle(requestinfo.request);
lock (responses)
{
responses[requestID] = new aPollResponse()
{
bytes = (int)response["int_bytes"],
response = response
};
}
m_throttler.ProcessTime();
}
internal void UpdateThrottle(int pimagethrottle)
{
m_throttler.ThrottleBytes = pimagethrottle;
}
}
public void RegisterCaps(UUID agentID, Caps caps) public void RegisterCaps(UUID agentID, Caps caps)
{ {
// UUID capID = UUID.Random(); // UUID capID = UUID.Random();
//caps.RegisterHandler("GetTexture", new StreamHandler("GET", "/CAPS/" + capID, ProcessGetTexture));
if (m_URL == "localhost") if (m_URL == "localhost")
{ {
// m_log.DebugFormat("[GETMESH]: /CAPS/{0} in region {1}", capID, m_scene.RegionInfo.RegionName); string capUrl = "/CAPS/" + UUID.Random() + "/";
GetMeshHandler gmeshHandler = new GetMeshHandler(m_AssetService);
IRequestHandler reqHandler
= new RestHTTPHandler(
"GET",
"/CAPS/" + UUID.Random(),
httpMethod => gmeshHandler.ProcessGetMesh(httpMethod, UUID.Zero, null),
"GetMesh",
agentID.ToString());
caps.RegisterHandler("GetMesh", reqHandler); // Register this as a poll service
PollServiceMeshEventArgs args = new PollServiceMeshEventArgs(agentID, m_scene);
args.Type = PollServiceEventArgs.EventType.Mesh;
MainServer.Instance.AddPollServiceHTTPHandler(capUrl, args);
string hostName = m_scene.RegionInfo.ExternalHostName;
uint port = (MainServer.Instance == null) ? 0 : MainServer.Instance.Port;
string protocol = "http";
if (MainServer.Instance.UseSSL)
{
hostName = MainServer.Instance.SSLCommonName;
port = MainServer.Instance.SSLPort;
protocol = "https";
}
caps.RegisterHandler("GetMesh", String.Format("{0}://{1}:{2}{3}", protocol, hostName, port, capUrl));
m_pollservices.Add(agentID, args);
m_capsDict[agentID] = capUrl;
} }
else else
{ {
@ -136,6 +383,95 @@ namespace OpenSim.Region.ClientStack.Linden
caps.RegisterHandler("GetMesh", m_URL); caps.RegisterHandler("GetMesh", m_URL);
} }
} }
private void DeregisterCaps(UUID agentID, Caps caps)
{
string capUrl;
PollServiceMeshEventArgs args;
if (m_capsDict.TryGetValue(agentID, out capUrl))
{
MainServer.Instance.RemoveHTTPHandler("", capUrl);
m_capsDict.Remove(agentID);
}
if (m_pollservices.TryGetValue(agentID, out args))
{
m_pollservices.Remove(agentID);
}
}
internal sealed class CapsDataThrottler
{
private volatile int currenttime = 0;
private volatile int lastTimeElapsed = 0;
private volatile int BytesSent = 0;
private int oversizedImages = 0;
public CapsDataThrottler(int pBytes, int max, int min)
{
ThrottleBytes = pBytes;
lastTimeElapsed = Util.EnvironmentTickCount();
}
public bool hasEvents(UUID key, Dictionary<UUID, aPollResponse> responses)
{
PassTime();
// Note, this is called IN LOCK
bool haskey = responses.ContainsKey(key);
if (!haskey)
{
return false;
}
aPollResponse response;
if (responses.TryGetValue(key, out response))
{
// Normal
if (BytesSent + response.bytes <= ThrottleBytes)
{
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + 1000, unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
// Big textures
else if (response.bytes > ThrottleBytes && oversizedImages <= ((ThrottleBytes % 50000) + 1))
{
Interlocked.Increment(ref oversizedImages);
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + (((response.bytes % ThrottleBytes)+1)*1000) , unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
else
{
return false;
}
}
return haskey;
}
public void ProcessTime()
{
PassTime();
}
private void PassTime()
{
currenttime = Util.EnvironmentTickCount();
int timeElapsed = Util.EnvironmentTickCountSubtract(currenttime, lastTimeElapsed);
//processTimeBasedActions(responses);
if (Util.EnvironmentTickCountSubtract(currenttime, timeElapsed) >= 1000)
{
lastTimeElapsed = Util.EnvironmentTickCount();
BytesSent -= ThrottleBytes;
if (BytesSent < 0) BytesSent = 0;
if (BytesSent < ThrottleBytes)
{
oversizedImages = 0;
}
}
}
public int ThrottleBytes;
}
} }
} }

View File

@ -361,80 +361,81 @@ namespace OpenSim.Region.ClientStack.Linden
poolreq.thepoll.Process(poolreq); poolreq.thepoll.Process(poolreq);
} }
} }
} internal sealed class CapsDataThrottler
{
internal sealed class CapsDataThrottler private volatile int currenttime = 0;
{ private volatile int lastTimeElapsed = 0;
private volatile int BytesSent = 0;
private volatile int currenttime = 0; private int oversizedImages = 0;
private volatile int lastTimeElapsed = 0; public CapsDataThrottler(int pBytes, int max, int min)
private volatile int BytesSent = 0;
private int oversizedImages = 0;
public CapsDataThrottler(int pBytes, int max, int min)
{
ThrottleBytes = pBytes;
lastTimeElapsed = Util.EnvironmentTickCount();
}
public bool hasEvents(UUID key, Dictionary<UUID, GetTextureModule.aPollResponse> responses)
{
PassTime();
// Note, this is called IN LOCK
bool haskey = responses.ContainsKey(key);
if (!haskey)
{ {
return false; ThrottleBytes = pBytes;
lastTimeElapsed = Util.EnvironmentTickCount();
} }
GetTextureModule.aPollResponse response; public bool hasEvents(UUID key, Dictionary<UUID, GetTextureModule.aPollResponse> responses)
if (responses.TryGetValue(key,out response))
{ {
PassTime();
// Normal // Note, this is called IN LOCK
if (BytesSent + response.bytes <= ThrottleBytes) bool haskey = responses.ContainsKey(key);
{ if (!haskey)
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + 1000, unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
// Big textures
else if (response.bytes > ThrottleBytes && oversizedImages <= ((ThrottleBytes%50000) + 1))
{
Interlocked.Increment(ref oversizedImages);
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + (((response.bytes % ThrottleBytes)+1)*1000) , unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
else
{ {
return false; return false;
} }
GetTextureModule.aPollResponse response;
if (responses.TryGetValue(key, out response))
{
// Normal
if (BytesSent + response.bytes <= ThrottleBytes)
{
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + 1000, unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
// Big textures
else if (response.bytes > ThrottleBytes && oversizedImages <= ((ThrottleBytes % 50000) + 1))
{
Interlocked.Increment(ref oversizedImages);
BytesSent += response.bytes;
//TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + (((response.bytes % ThrottleBytes)+1)*1000) , unlockyn = false };
//m_actions.Add(timeBasedAction);
return true;
}
else
{
return false;
}
}
return haskey;
}
public void ProcessTime()
{
PassTime();
} }
return haskey;
} private void PassTime()
public void ProcessTime()
{
PassTime();
}
private void PassTime()
{
currenttime = Util.EnvironmentTickCount();
int timeElapsed = Util.EnvironmentTickCountSubtract(currenttime, lastTimeElapsed);
//processTimeBasedActions(responses);
if (Util.EnvironmentTickCountSubtract(currenttime, timeElapsed) >= 1000)
{ {
lastTimeElapsed = Util.EnvironmentTickCount(); currenttime = Util.EnvironmentTickCount();
BytesSent -= ThrottleBytes; int timeElapsed = Util.EnvironmentTickCountSubtract(currenttime, lastTimeElapsed);
if (BytesSent < 0) BytesSent = 0; //processTimeBasedActions(responses);
if (BytesSent < ThrottleBytes) if (Util.EnvironmentTickCountSubtract(currenttime, timeElapsed) >= 1000)
{ {
oversizedImages = 0; lastTimeElapsed = Util.EnvironmentTickCount();
BytesSent -= ThrottleBytes;
if (BytesSent < 0) BytesSent = 0;
if (BytesSent < ThrottleBytes)
{
oversizedImages = 0;
}
} }
} }
public int ThrottleBytes;
} }
public int ThrottleBytes;
} }
} }