aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs
diff options
context:
space:
mode:
authorAdam Frisby2008-05-02 16:41:08 +0000
committerAdam Frisby2008-05-02 16:41:08 +0000
commit29b8c84ceaaeca80bfa9c0bfc8c31e421e39ef71 (patch)
tree848ce119d9b87bc186c20b08cc604de50a872b31 /OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs
parent* Refactored ClientView into LLClientView. Removed all direct references to U... (diff)
downloadopensim-SC_OLD-29b8c84ceaaeca80bfa9c0bfc8c31e421e39ef71.zip
opensim-SC_OLD-29b8c84ceaaeca80bfa9c0bfc8c31e421e39ef71.tar.gz
opensim-SC_OLD-29b8c84ceaaeca80bfa9c0bfc8c31e421e39ef71.tar.bz2
opensim-SC_OLD-29b8c84ceaaeca80bfa9c0bfc8c31e421e39ef71.tar.xz
* Commit 2/3 - Please dont attempt to update to this revision until all 3 are in.
Diffstat (limited to 'OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs')
-rw-r--r--OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs532
1 files changed, 532 insertions, 0 deletions
diff --git a/OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs b/OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs
new file mode 100644
index 0000000..5dd1da6
--- /dev/null
+++ b/OpenSim/Region/ClientStack/LindenUDP/LLPacketQueue.cs
@@ -0,0 +1,532 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSim Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.Collections.Generic;
30using System.Threading;
31using System.Timers;
32using libsecondlife;
33using libsecondlife.Packets;
34using OpenSim.Framework;
35using OpenSim.Framework.Statistics;
36using OpenSim.Framework.Statistics.Interfaces;
37using Timer=System.Timers.Timer;
38
39namespace OpenSim.Region.ClientStack.LindenUDP
40{
41 public class LLPacketQueue : IPullStatsProvider
42 {
43 //private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
44
45 private bool m_enabled = true;
46
47 private BlockingQueue<LLQueItem> SendQueue;
48
49 private Queue<LLQueItem> IncomingPacketQueue;
50 private Queue<LLQueItem> OutgoingPacketQueue;
51 private Queue<LLQueItem> ResendOutgoingPacketQueue;
52 private Queue<LLQueItem> LandOutgoingPacketQueue;
53 private Queue<LLQueItem> WindOutgoingPacketQueue;
54 private Queue<LLQueItem> CloudOutgoingPacketQueue;
55 private Queue<LLQueItem> TaskOutgoingPacketQueue;
56 private Queue<LLQueItem> TextureOutgoingPacketQueue;
57 private Queue<LLQueItem> AssetOutgoingPacketQueue;
58
59 private Dictionary<uint, uint> PendingAcks = new Dictionary<uint, uint>();
60 private Dictionary<uint, Packet> NeedAck = new Dictionary<uint, Packet>();
61
62 // All throttle times and number of bytes are calculated by dividing by this value
63 // This value also determines how many times per throttletimems the timer will run
64 // If throttleimems is 1000 ms, then the timer will fire every 1000/7 milliseconds
65
66 private int throttleTimeDivisor = 7;
67
68 private int throttletimems = 1000;
69
70 private LLPacketThrottle ResendThrottle;
71 private LLPacketThrottle LandThrottle;
72 private LLPacketThrottle WindThrottle;
73 private LLPacketThrottle CloudThrottle;
74 private LLPacketThrottle TaskThrottle;
75 private LLPacketThrottle AssetThrottle;
76 private LLPacketThrottle TextureThrottle;
77 private LLPacketThrottle TotalThrottle;
78
79 // private long LastThrottle;
80 // private long ThrottleInterval;
81 private Timer throttleTimer;
82
83 private LLUUID m_agentId;
84
85 public LLPacketQueue(LLUUID agentId)
86 {
87 // While working on this, the BlockingQueue had me fooled for a bit.
88 // The Blocking queue causes the thread to stop until there's something
89 // in it to process. it's an on-purpose threadlock though because
90 // without it, the clientloop will suck up all sim resources.
91
92 SendQueue = new BlockingQueue<LLQueItem>();
93
94 IncomingPacketQueue = new Queue<LLQueItem>();
95 OutgoingPacketQueue = new Queue<LLQueItem>();
96 ResendOutgoingPacketQueue = new Queue<LLQueItem>();
97 LandOutgoingPacketQueue = new Queue<LLQueItem>();
98 WindOutgoingPacketQueue = new Queue<LLQueItem>();
99 CloudOutgoingPacketQueue = new Queue<LLQueItem>();
100 TaskOutgoingPacketQueue = new Queue<LLQueItem>();
101 TextureOutgoingPacketQueue = new Queue<LLQueItem>();
102 AssetOutgoingPacketQueue = new Queue<LLQueItem>();
103
104
105 // Set up the throttle classes (min, max, current) in bytes
106 ResendThrottle = new LLPacketThrottle(5000, 100000, 16000);
107 LandThrottle = new LLPacketThrottle(1000, 100000, 2000);
108 WindThrottle = new LLPacketThrottle(1000, 100000, 1000);
109 CloudThrottle = new LLPacketThrottle(1000, 100000, 1000);
110 TaskThrottle = new LLPacketThrottle(1000, 800000, 3000);
111 AssetThrottle = new LLPacketThrottle(1000, 800000, 1000);
112 TextureThrottle = new LLPacketThrottle(1000, 800000, 4000);
113 // Total Throttle trumps all
114 // Number of bytes allowed to go out per second. (256kbps per client)
115 TotalThrottle = new LLPacketThrottle(0, 1500000, 28000);
116
117 throttleTimer = new Timer((int) (throttletimems/throttleTimeDivisor));
118 throttleTimer.Elapsed += new ElapsedEventHandler(ThrottleTimerElapsed);
119 throttleTimer.Start();
120
121 // TIMERS needed for this
122 // LastThrottle = DateTime.Now.Ticks;
123 // ThrottleInterval = (long)(throttletimems/throttleTimeDivisor);
124
125 m_agentId = agentId;
126
127 if (StatsManager.SimExtraStats != null)
128 {
129 StatsManager.SimExtraStats.RegisterPacketQueueStatsProvider(m_agentId, this);
130 }
131 }
132
133 /* STANDARD QUEUE MANIPULATION INTERFACES */
134
135
136 public void Enqueue(LLQueItem item)
137 {
138 if (!m_enabled)
139 {
140 return;
141 }
142 // We could micro lock, but that will tend to actually
143 // probably be worse than just synchronizing on SendQueue
144
145 if (item == null)
146 {
147 SendQueue.Enqueue(item);
148 return;
149 }
150
151 lock (this) {
152 switch (item.throttleType)
153 {
154 case ThrottleOutPacketType.Resend:
155 ThrottleCheck(ref ResendThrottle, ref ResendOutgoingPacketQueue, item);
156 break;
157 case ThrottleOutPacketType.Texture:
158 ThrottleCheck(ref TextureThrottle, ref TextureOutgoingPacketQueue, item);
159 break;
160 case ThrottleOutPacketType.Task:
161 ThrottleCheck(ref TaskThrottle, ref TaskOutgoingPacketQueue, item);
162 break;
163 case ThrottleOutPacketType.Land:
164 ThrottleCheck(ref LandThrottle, ref LandOutgoingPacketQueue, item);
165 break;
166 case ThrottleOutPacketType.Asset:
167 ThrottleCheck(ref AssetThrottle, ref AssetOutgoingPacketQueue, item);
168 break;
169 case ThrottleOutPacketType.Cloud:
170 ThrottleCheck(ref CloudThrottle, ref CloudOutgoingPacketQueue, item);
171 break;
172 case ThrottleOutPacketType.Wind:
173 ThrottleCheck(ref WindThrottle, ref WindOutgoingPacketQueue, item);
174 break;
175
176 default:
177 // Acknowledgements and other such stuff should go directly to the blocking Queue
178 // Throttling them may and likely 'will' be problematic
179 SendQueue.Enqueue(item);
180 break;
181 }
182 }
183 }
184
185 public LLQueItem Dequeue()
186 {
187 return SendQueue.Dequeue();
188 }
189
190 public void Flush()
191 {
192 lock (this)
193 {
194 while (PacketsWaiting())
195 {
196 //Now comes the fun part.. we dump all our elements into m_packetQueue that we've saved up.
197 if (ResendOutgoingPacketQueue.Count > 0)
198 {
199 SendQueue.Enqueue(ResendOutgoingPacketQueue.Dequeue());
200 }
201 if (LandOutgoingPacketQueue.Count > 0)
202 {
203 SendQueue.Enqueue(LandOutgoingPacketQueue.Dequeue());
204 }
205 if (WindOutgoingPacketQueue.Count > 0)
206 {
207 SendQueue.Enqueue(WindOutgoingPacketQueue.Dequeue());
208 }
209 if (CloudOutgoingPacketQueue.Count > 0)
210 {
211 SendQueue.Enqueue(CloudOutgoingPacketQueue.Dequeue());
212 }
213 if (TaskOutgoingPacketQueue.Count > 0)
214 {
215 SendQueue.Enqueue(TaskOutgoingPacketQueue.Dequeue());
216 }
217 if (TextureOutgoingPacketQueue.Count > 0)
218 {
219 SendQueue.Enqueue(TextureOutgoingPacketQueue.Dequeue());
220 }
221 if (AssetOutgoingPacketQueue.Count > 0)
222 {
223 SendQueue.Enqueue(AssetOutgoingPacketQueue.Dequeue());
224 }
225 }
226 // m_log.Info("[THROTTLE]: Processed " + throttleLoops + " packets");
227 }
228 }
229
230 public void Close()
231 {
232 Flush();
233
234 m_enabled = false;
235 throttleTimer.Stop();
236
237 if (StatsManager.SimExtraStats != null)
238 {
239 StatsManager.SimExtraStats.DeregisterPacketQueueStatsProvider(m_agentId);
240 }
241 }
242
243 private void ResetCounters()
244 {
245 ResendThrottle.Reset();
246 LandThrottle.Reset();
247 WindThrottle.Reset();
248 CloudThrottle.Reset();
249 TaskThrottle.Reset();
250 AssetThrottle.Reset();
251 TextureThrottle.Reset();
252 TotalThrottle.Reset();
253 }
254
255 private bool PacketsWaiting()
256 {
257 return (ResendOutgoingPacketQueue.Count > 0 ||
258 LandOutgoingPacketQueue.Count > 0 ||
259 WindOutgoingPacketQueue.Count > 0 ||
260 CloudOutgoingPacketQueue.Count > 0 ||
261 TaskOutgoingPacketQueue.Count > 0 ||
262 AssetOutgoingPacketQueue.Count > 0 ||
263 TextureOutgoingPacketQueue.Count > 0);
264 }
265
266 public void ProcessThrottle()
267 {
268 // I was considering this.. Will an event fire if the thread it's on is blocked?
269
270 // Then I figured out.. it doesn't really matter.. because this thread won't be blocked for long
271 // The General overhead of the UDP protocol gets sent to the queue un-throttled by this
272 // so This'll pick up about around the right time.
273
274 int MaxThrottleLoops = 4550; // 50*7 packets can be dequeued at once.
275 int throttleLoops = 0;
276
277 // We're going to dequeue all of the saved up packets until
278 // we've hit the throttle limit or there's no more packets to send
279 lock (this)
280 {
281 ResetCounters();
282 // m_log.Info("[THROTTLE]: Entering Throttle");
283 while (TotalThrottle.UnderLimit() && PacketsWaiting() &&
284 (throttleLoops <= MaxThrottleLoops))
285 {
286 throttleLoops++;
287 //Now comes the fun part.. we dump all our elements into m_packetQueue that we've saved up.
288 if (ResendThrottle.UnderLimit() && ResendOutgoingPacketQueue.Count > 0)
289 {
290 LLQueItem qpack = ResendOutgoingPacketQueue.Dequeue();
291
292 SendQueue.Enqueue(qpack);
293 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
294 ResendThrottle.Add(qpack.Packet.ToBytes().Length);
295 }
296 if (LandThrottle.UnderLimit() && LandOutgoingPacketQueue.Count > 0)
297 {
298 LLQueItem qpack = LandOutgoingPacketQueue.Dequeue();
299
300 SendQueue.Enqueue(qpack);
301 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
302 LandThrottle.Add(qpack.Packet.ToBytes().Length);
303 }
304 if (WindThrottle.UnderLimit() && WindOutgoingPacketQueue.Count > 0)
305 {
306 LLQueItem qpack = WindOutgoingPacketQueue.Dequeue();
307
308 SendQueue.Enqueue(qpack);
309 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
310 WindThrottle.Add(qpack.Packet.ToBytes().Length);
311 }
312 if (CloudThrottle.UnderLimit() && CloudOutgoingPacketQueue.Count > 0)
313 {
314 LLQueItem qpack = CloudOutgoingPacketQueue.Dequeue();
315
316 SendQueue.Enqueue(qpack);
317 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
318 CloudThrottle.Add(qpack.Packet.ToBytes().Length);
319 }
320 if (TaskThrottle.UnderLimit() && TaskOutgoingPacketQueue.Count > 0)
321 {
322 LLQueItem qpack = TaskOutgoingPacketQueue.Dequeue();
323
324 SendQueue.Enqueue(qpack);
325 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
326 TaskThrottle.Add(qpack.Packet.ToBytes().Length);
327 }
328 if (TextureThrottle.UnderLimit() && TextureOutgoingPacketQueue.Count > 0)
329 {
330 LLQueItem qpack = TextureOutgoingPacketQueue.Dequeue();
331
332 SendQueue.Enqueue(qpack);
333 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
334 TextureThrottle.Add(qpack.Packet.ToBytes().Length);
335 }
336 if (AssetThrottle.UnderLimit() && AssetOutgoingPacketQueue.Count > 0)
337 {
338 LLQueItem qpack = AssetOutgoingPacketQueue.Dequeue();
339
340 SendQueue.Enqueue(qpack);
341 TotalThrottle.Add(qpack.Packet.ToBytes().Length);
342 AssetThrottle.Add(qpack.Packet.ToBytes().Length);
343 }
344 }
345 // m_log.Info("[THROTTLE]: Processed " + throttleLoops + " packets");
346 }
347 }
348
349 private void ThrottleTimerElapsed(object sender, ElapsedEventArgs e)
350 {
351 // just to change the signature, and that ProcessThrottle
352 // will be used elsewhere possibly
353 ProcessThrottle();
354 }
355
356 private void ThrottleCheck(ref LLPacketThrottle throttle, ref Queue<LLQueItem> q, LLQueItem item)
357 {
358 // The idea.. is if the packet throttle queues are empty
359 // and the client is under throttle for the type. Queue
360 // it up directly. This basically short cuts having to
361 // wait for the timer to fire to put things into the
362 // output queue
363
364 if ((q.Count == 0) && (throttle.UnderLimit()))
365 {
366 Monitor.Enter(this);
367 throttle.Add(item.Packet.ToBytes().Length);
368 TotalThrottle.Add(item.Packet.ToBytes().Length);
369 SendQueue.Enqueue(item);
370 Monitor.Pulse(this);
371 Monitor.Exit(this);
372 }
373 else
374 {
375 q.Enqueue(item);
376 }
377 }
378
379
380 private static int ScaleThrottle(int value, int curmax, int newmax)
381 {
382 return (value / curmax) * newmax;
383 }
384
385 public byte[] GetThrottlesPacked(float multiplier)
386 {
387 int singlefloat = 4;
388 float tResend = ResendThrottle.Throttle*multiplier;
389 float tLand = LandThrottle.Throttle*multiplier;
390 float tWind = WindThrottle.Throttle*multiplier;
391 float tCloud = CloudThrottle.Throttle*multiplier;
392 float tTask = TaskThrottle.Throttle*multiplier;
393 float tTexture = TextureThrottle.Throttle*multiplier;
394 float tAsset = AssetThrottle.Throttle*multiplier;
395
396 byte[] throttles = new byte[singlefloat*7];
397 int i = 0;
398 Buffer.BlockCopy(BitConverter.GetBytes(tResend), 0, throttles, singlefloat*i, singlefloat);
399 i++;
400 Buffer.BlockCopy(BitConverter.GetBytes(tLand), 0, throttles, singlefloat*i, singlefloat);
401 i++;
402 Buffer.BlockCopy(BitConverter.GetBytes(tWind), 0, throttles, singlefloat*i, singlefloat);
403 i++;
404 Buffer.BlockCopy(BitConverter.GetBytes(tCloud), 0, throttles, singlefloat*i, singlefloat);
405 i++;
406 Buffer.BlockCopy(BitConverter.GetBytes(tTask), 0, throttles, singlefloat*i, singlefloat);
407 i++;
408 Buffer.BlockCopy(BitConverter.GetBytes(tTexture), 0, throttles, singlefloat*i, singlefloat);
409 i++;
410 Buffer.BlockCopy(BitConverter.GetBytes(tAsset), 0, throttles, singlefloat*i, singlefloat);
411
412 return throttles;
413 }
414
415 public void SetThrottleFromClient(byte[] throttle)
416 {
417 int tResend = -1;
418 int tLand = -1;
419 int tWind = -1;
420 int tCloud = -1;
421 int tTask = -1;
422 int tTexture = -1;
423 int tAsset = -1;
424 int tall = -1;
425 int singlefloat = 4;
426
427 //Agent Throttle Block contains 7 single floatingpoint values.
428 int j = 0;
429
430 // Some Systems may be big endian...
431 // it might be smart to do this check more often...
432 if (!BitConverter.IsLittleEndian)
433 for (int i = 0; i < 7; i++)
434 Array.Reverse(throttle, j + i*singlefloat, singlefloat);
435
436 // values gotten from libsecondlife.org/wiki/Throttle. Thanks MW_
437 // bytes
438 // Convert to integer, since.. the full fp space isn't used.
439 tResend = (int) BitConverter.ToSingle(throttle, j);
440 j += singlefloat;
441 tLand = (int) BitConverter.ToSingle(throttle, j);
442 j += singlefloat;
443 tWind = (int) BitConverter.ToSingle(throttle, j);
444 j += singlefloat;
445 tCloud = (int) BitConverter.ToSingle(throttle, j);
446 j += singlefloat;
447 tTask = (int) BitConverter.ToSingle(throttle, j);
448 j += singlefloat;
449 tTexture = (int) BitConverter.ToSingle(throttle, j);
450 j += singlefloat;
451 tAsset = (int) BitConverter.ToSingle(throttle, j);
452
453 tall = tResend + tLand + tWind + tCloud + tTask + tTexture + tAsset;
454 /*
455 m_log.Info("[CLIENT]: Client AgentThrottle - Got throttle:resendbytes=" + tResend +
456 " landbytes=" + tLand +
457 " windbytes=" + tWind +
458 " cloudbytes=" + tCloud +
459 " taskbytes=" + tTask +
460 " texturebytes=" + tTexture +
461 " Assetbytes=" + tAsset +
462 " Allbytes=" + tall);
463 */
464
465 // Total Sanity
466 // Make sure that the client sent sane total values.
467
468 // If the client didn't send acceptable values....
469 // Scale the clients values down until they are acceptable.
470
471 if (tall <= TotalThrottle.Max)
472 {
473 ResendThrottle.Throttle = tResend;
474 LandThrottle.Throttle = tLand;
475 WindThrottle.Throttle = tWind;
476 CloudThrottle.Throttle = tCloud;
477 TaskThrottle.Throttle = tTask;
478 TextureThrottle.Throttle = tTexture;
479 AssetThrottle.Throttle = tAsset;
480 TotalThrottle.Throttle = tall;
481 }
482 else if (tall < 1)
483 {
484 // client is stupid, penalize him by minning everything
485 ResendThrottle.Throttle = ResendThrottle.Min;
486 LandThrottle.Throttle = LandThrottle.Min;
487 WindThrottle.Throttle = WindThrottle.Min;
488 CloudThrottle.Throttle = CloudThrottle.Min;
489 TaskThrottle.Throttle = TaskThrottle.Min;
490 TextureThrottle.Throttle = TextureThrottle.Min;
491 AssetThrottle.Throttle = AssetThrottle.Min;
492 TotalThrottle.Throttle = TotalThrottle.Min;
493 }
494 else
495 {
496 // we're over so figure out percentages and use those
497 ResendThrottle.Throttle = tResend;
498
499 LandThrottle.Throttle = ScaleThrottle(tLand, tall, TotalThrottle.Max);
500 WindThrottle.Throttle = ScaleThrottle(tWind, tall, TotalThrottle.Max);
501 CloudThrottle.Throttle = ScaleThrottle(tCloud, tall, TotalThrottle.Max);
502 TaskThrottle.Throttle = ScaleThrottle(tTask, tall, TotalThrottle.Max);
503 TextureThrottle.Throttle = ScaleThrottle(tTexture, tall, TotalThrottle.Max);
504 AssetThrottle.Throttle = ScaleThrottle(tAsset, tall, TotalThrottle.Max);
505 TotalThrottle.Throttle = TotalThrottle.Max;
506 }
507 // effectively wiggling the slider causes things reset
508 ResetCounters();
509 }
510
511 // See IPullStatsProvider
512 public string GetStats()
513 {
514 return string.Format("{0,7} {1,7} {2,7} {3,7} {4,7} {5,7} {6,7} {7,7} {8,7} {9,7}",
515 SendQueue.Count(),
516 IncomingPacketQueue.Count,
517 OutgoingPacketQueue.Count,
518 ResendOutgoingPacketQueue.Count,
519 LandOutgoingPacketQueue.Count,
520 WindOutgoingPacketQueue.Count,
521 CloudOutgoingPacketQueue.Count,
522 TaskOutgoingPacketQueue.Count,
523 TextureOutgoingPacketQueue.Count,
524 AssetOutgoingPacketQueue.Count);
525 }
526
527 public LLQueItem[] GetQueueArray()
528 {
529 return SendQueue.GetQueueArray();
530 }
531 }
532} \ No newline at end of file