Monday, August 15, 2011

You Know Its TIme to Update When...

You know its time to update when you can't remember the last time Rhythmbox found the album information of the CDs you've inserted. Your Rhythmbox is too old; why are you running Ubuntu 9.10 in 2011?

-- UPDATE --
Damnit, latest Ubuntu has the same damn problem (Rhythmbox 0.13.3), 404 on the request. Example after 0.12.5 example.
-- END UPDATE --

Ye Olde Rhythmbox 0.12.5 is trying to use an old script on musicbrainz.org that doesn't exist anymore. The following is a capture of the failing request to obtain the album information.

POST /cgi%2dbin/mq%5f2%5f1.pl HTTP/1.0
Host: mm.musicbrainz.org
Accept: */*
User-Agent: libmusicbrainz/2.1.5
Content-type: text/plain
Content-length: 2484

<?xml version="1.0"?>
<rdf:RDF xmlns:rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc = "http://purl.org/dc/elements/1.1/"
xmlns:mq = "http://musicbrainz.org/mm/mq-1.1#"
xmlns:mm = "http://musicbrainz.org/mm/mm-2.1#">
<mq:GetCDInfo>
<mq:depth>2</mq:depth>
<mm:cdindexid>q85Jz8YHGZs3pra.5mD0bQAqtWo-</mm:cdindexid>
<mm:firstTrack>1</mm:firstTrack>
<mm:lastTrack>10</mm:lastTrack>
<mm:toc>
<rdf:Seq>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>183530</mm:sectorOffset>
<mm:numSectors>0</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>150</mm:sectorOffset>
<mm:numSectors>22299</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>22449</mm:sectorOffset>
<mm:numSectors>16842</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>39291</mm:sectorOffset>
<mm:numSectors>17599</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>56890</mm:sectorOffset>
<mm:numSectors>15427</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>72317</mm:sectorOffset>
<mm:numSectors>16560</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>88877</mm:sectorOffset>
<mm:numSectors>17856</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>106733</mm:sectorOffset>
<mm:numSectors>20088</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>126821</mm:sectorOffset>
<mm:numSectors>28970</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>155791</mm:sectorOffset>
<mm:numSectors>15128</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>170919</mm:sectorOffset>
<mm:numSectors>12611</mm:numSectors>
</mm:TocInfo>
</rdf:li>
</rdf:Seq>
</mm:toc>
</mq:GetCDInfo>

</rdf:RDF>
HTTP/1.1 404
Date: Mon, 15 Aug 2011 04:31:09 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Server: nginx/0.7.65
Content-Length: 10108
Set-Cookie: musicbrainz_server_session=whatever; path=/; expires=Mon, 15-Aug-2011 06:31:09 GMT; HttpOnly
Set-Cookie: javascript=false; path=/

And here's the latest Natty one (Rhythmbox 0.13.3):

POST /cgi%2dbin/mq%5f2%5f1.pl HTTP/1.0
Host: mm.musicbrainz.org
Accept: */*
User-Agent: libmusicbrainz/2.1.5
Content-type: text/plain
Content-length: 2484

<?xml version="1.0"?>
<rdf:RDF xmlns:rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc = "http://purl.org/dc/elements/1.1/"
xmlns:mq = "http://musicbrainz.org/mm/mq-1.1#"
xmlns:mm = "http://musicbrainz.org/mm/mm-2.1#">
<mq:GetCDInfo>
<mq:depth>2</mq:depth>
<mm:cdindexid>q85Jz8YHGZs3pra.5mD0bQAqtWo-</mm:cdindexid>
<mm:firstTrack>1</mm:firstTrack>
<mm:lastTrack>10</mm:lastTrack>
<mm:toc>
<rdf:Seq>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>183530</mm:sectorOffset>
<mm:numSectors>0</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>150</mm:sectorOffset>
<mm:numSectors>22299</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>22449</mm:sectorOffset>
<mm:numSectors>16842</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>39291</mm:sectorOffset>
<mm:numSectors>17599</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>56890</mm:sectorOffset>
<mm:numSectors>15427</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>72317</mm:sectorOffset>
<mm:numSectors>16560</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>88877</mm:sectorOffset>
<mm:numSectors>17856</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>106733</mm:sectorOffset>
<mm:numSectors>20088</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>126821</mm:sectorOffset>
<mm:numSectors>28970</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>155791</mm:sectorOffset>
<mm:numSectors>15128</mm:numSectors>
</mm:TocInfo>
</rdf:li>
<rdf:li>
<mm:TocInfo>
<mm:sectorOffset>170919</mm:sectorOffset>
<mm:numSectors>12611</mm:numSectors>
</mm:TocInfo>
</rdf:li>
</rdf:Seq>
</mm:toc>
</mq:GetCDInfo>

</rdf:RDF>
HTTP/1.1 404
Date: Mon, 15 Aug 2011 14:40:53 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Server: nginx/0.7.65
Content-Length: 10108
Set-Cookie: musicbrainz_server_session=removed; path=/; expires=Mon, 15-Aug-2011 16:40:53 GMT; HttpOnly
Set-Cookie: javascript=false; path=/





Friday, August 6, 2010

Interaction with Firefox Download Manager

I would like a torrent download to be managed by the Download window for stopping/pausing/progress/opening-file. To this end, it appears nsIDownloadManager.addDownload would be appropriate. But there be dragons.

The method requires the passing of a URI to be downloaded, but that doesn't quite fit with the manner of Bittorrent. Perhaps an appropriate URI would the location of the .torrent file, but that isn't true - for one, if this was used the Download Manager would just download the .torrent file, and I can't link this to the download of the 'real' torrent, so hitting pause/stop/etc. will not have a connection with the real torrent download.

A second issue is that the addDownload method takes a destination file. It's rare to see a torrent contain just one file, so this again is an impossibility. I expect the download manager is expecting a nice channel to read from and then feed this into one file. Not so good for my situation.

So, I have an idea. One part is the representation of a torrent by using a URI in the form of a magnet-URI. This way, I can pass the nsIDownloadManager a magnet: URI which will get the Download Manager to call the magnet protocol handler which will feed the Download Manager data from the real torrent. Yay, just need to implement a magnet: protocol handler.

This however doesn't help with the one destination file problem. So, the second part of this is to split a torrent into multiple URIs. So, given the example of this torrent: http://www.mininova.org/det/3190112
  • The current magnet URI is magnet:?xt=urn:btih:25H6RSS2GDS4BT235LTX7IE4UMNOCEM7&tr=http://tracker.mininova.org/announce
I'm offering the following URIs for the individual files in the torrent:
  • magnet:/01%201%20Mist.mp3?xt=urn:btih:25H6RSS2GDS4BT235LTX7IE4UMNOCEM7&tr=http://tracker.mininova.org/announce
  • magnet:/2%20Vietcong Blues%20.mp3?xt=urn:btih:25H6RSS2GDS4BT235LTX7IE4UMNOCEM7&tr=http://tracker.mininova.org/announce
  • magnet:/3%20Chill%20Song.mp3?xt=urn:btih:25H6RSS2GDS4BT235LTX7IE4UMNOCEM7&tr=http://tracker.mininova.org/announce
  • etc.
So now when a torrent is opened, each item will appear in the download window. This would work well for interactions with the addDownload call to nsIDownloadManager: each item has a unique source and destination address. Also, it provides an easy manner for removing files in a torrent without another GUI.
But... this isn't a normal magnet URI, it contains paths (which given a bittorrent source, seems reasonable... though again this isn't normal). As it is used in a closed system maybe it's okay...

Thoughts? Hmm? The key problem I see is that if I start using magnet URIs, I probably need to support them properly, at least for the ones related to Bittorrent. But that's a lot of work, as it requires implementing BEP 0005, BEP 0009 and BEP 0010.

Thursday, August 5, 2010

Tad tired with Bittorrent/Firefox

Last night added:
  • timers to keep in contact with Tracker after first call (prior to this, called Tracker once)
  • set a minimum interval for the above to 2 minutes, otherwise uses the interval received from the tracker
  • will now retry a disconnected peer if it appears in a tracker response received after a 10 minute cool down.
  • notification on completed torrent (alert)
  • if Firefox is shutdown, saves state of torrent download to a status file in the folder the torrent is downloading to
  • if a .torrent is opened, and then chosen to download to a folder with a status file that matches the infohash of the torrent, it will resume from the state in the status file.
  • timer for keepalive with peer
  • used existing counters for download/upload/remaining bytes
  • informing peers of completed pieces (sending it on keepalive, only informing if the peer doesn't have the piece)
Obvious missing things:
  • Listening to port (no server started or even implemented yet). Would like to listen on one port for multiple torrents.
  • No ability to control downloading (ie. stop it) except closing browser. Would like to use download manager.

Tuesday, July 20, 2010

Requestin' Blocks in Bittorrent (Part II)

I've worked on a XPCOM component to figure out what block (piece subsection) to request next. The component is probably complete, it's passed parsing and there are no exceptions on calling methods. I've got an idea for a set of tests, so perhaps I'll write that before releasing the code.

I expected to just write code to identify the next block to request, however it kept growing as the decision became influenced by more variables. In the end, the decision of what block to request came as a result of (i) what had already been downloaded, (ii) rarity/what has already been requested, and (iii) what messages have been sent/received from the peer. The third item grew the component so that it now functions by responding to peer messages. The table below summarizes how the component has been written to respond to incoming messages.

General behavior of bittorrent perhaps?
Received MessageResponse Message
BitfieldInterested (if peer has pieces we don't)
HaveInterested (if peer has pieces we don't & we haven't sent Interested)
Choke-
UnchokeRequest (if they still have pieces we don't) else Not Interested
InterestedUnchoke
Not InterestedChoke
PieceRequest (if they still have pieces we don't) else Not Interested
RequestPiece

What isn't covered here, though I think is probably appropriate, is a timeout associated with each peer that will either send Have messages of completed pieces since last timeout, or a keepalive. I haven't written the timeout, but provided it existed and worked in such a manner, there doesn't really need to be any more intelligence in the program, with the exception of handling bandwidth.

Saturday, July 17, 2010

Requesting blocks in Bittorrent

I'm working on perhaps my 4th iteration this week of an algorithm to decide the next block to request. This is kind of important; done poorly it leads to certain inefficiencies, such as getting the same block from many peers and not reducing rarity of pieces in the swarm.

I could look at the algorithm in use by other clients, and that would certainly be interesting. However, a particular algorithm is not required by the protocol, and it is a fun problem. Checking if this is covered in academia, and what research has occured would be interesting. Perhaps next Tuesday. For the moment, I've scrapped a couple of designs and mutated others; it's my type of puzzle, and I don't particularly want to have the journey spoilt - though there is an issue that the result will not be the most optimal (;-)), and not as tested as existing solutions.

I work on the idea that pieces have rarity, and if given the opportunity it would be best to request blocks from a rare piece rather than a common piece (yep, basic bittorrent). In order to complete a piece there needs to be several block requests (a block is an arbitrary segment which can be transferred between peers, and it can go over a piece boundary into the next piece). For efficiency from the requesting point of view, batch requesting several blocks is a good idea.

At some point there can be a peer available that can only help with a piece that has already started to be downloaded. This may be great; maybe the new peer has a fast connection. But it would be simpler if a piece was only being received from one peer. One of the issues I don't like is this: you're waiting on a particular block from peer A. Peer B could also do it, but for efficiency you don't want to ask a second peer. Time goes on, and peer B leaves the swarm. Time goes on, and your connection to Peer A is broken. Sure, it's a special case, and it perhaps doesn't alter the health of the swarm but it does potentially alter the acquisition time to get your content.

It's this kind of thing that bugs me, and has given rise to a few ideas in my planning. What piece should you target when you have a new peer? Maybe have a list of pieces not yet associated with any pending block request to other peers, and choose to work on one of those pieces first. If none of those is a valid option (ie. the list is empty, or only contains pieces this peer doesn't have), fall back to a list of pieces that don't have requests pending for all of the piece yet, then finally fall back to list of the oldest requests, or something similar, or ignore and download whatever is available with no regard for other requests with other peers.

I've started such a thing, and work on picking through missing bits of a piece for a potential block request, but this isn't the route I'm taking. Maybe a few ideas have been kept, however I've still disposed of more code than kept, and there's much more that needs to be written before it's complete.

At the moment it looks like I'm working with a priority queue of potential pieces, that move about based on rarity and current pending requests. The queue has survived the last major removal of code, though things at the level of picking the block request have been scrapped and restarted. The ability to batch request several blocks at a time forced the last scrapping of code; it's fairly easy to pick a single block at a time, but several blocks is painful. It may be okay to request the same block from a couple of peers (at some point), but I was heading in the direction of making the request for the same block from the same peer... and if catching this case, forcing it to consider anything else is kind of difficult, when making a request for an already pending block is valid in some cases. It seems like a simple problem, and it is, but for clarity it meant dumping code and a different approach.

I should probably only spend a day or two more on this, I don't want to lose pace releasing code. Days ago I felt I was in the same place of dealing with memory issues as I was some time ago, and it almost stopped me. Thankfully, these days I work on smaller pieces of code that can be tested independently and I could eventually reduce my problem to a single line of code. The fact I now have to frequently spam the garbage collector with requests to clean up sucks, but at least the problem of chewing huge chunks of memory receiving anything from a socket, even if it's never assigned to a JavaScript variable is handled. I was very, very stressed; I was in the same position I was in last year and all my options looked the same, and I didn't solve it last time. So relieved to track it to garbage collection.

Thursday, July 15, 2010

Info - Bittorrent Wire Protocol

Ohhkay. I'm not going to claim any great wisdom in this, but I will try to cover this component and what it does, with regards to the bittorrent wire protocol.

Specification, where?
Bittorrent.org's BEP 0003 covers the wire protocol, but the link to the section is crap. It's after this bit. Theory.org is a bit easier to digest in my opinion, starting with an introduction section.

Wire
After opening the TCP connection the two parties send a handshake, which provides their capabilities, identities and a confirmation of the torrent they are interested in (the info hash). Theoretically, the party opening the connection will send their handshake first, the party who opened the connection waiting until receiving the info hash before starting their part of the handshake. After this, the two parties use the connection in the same manner, sending messages to one another about what their state is and requesting parts of the torrent. If there's ever an error the connection should be broken.

Messages
Messages are structured with a 4 bytes header, which gives the length of the message, and for messages other than keepalive this is followed by a byte identifying the type of message.
The component informs an observer of the messsage id and the message payload after receiving a complete message. This is simpler than interfacing with the incoming stream directly.

Onto files:
Implementation of WireReader XPCOM component
IDL for WireReader XPCOM component

Example of usage:

// Attempt to connect to the peer
// These should be real values
var peerIpAddress = "127.0.0.1";
var peerPort = 6882;
var infoHash = String.fromCharCode(0,0,0,0,0);
infoHash = infoHash + infoHash + infoHash + infoHash;
var ourId = "01234567890123456789";
var remoteId = ""; // When remote peer id unknown, use empty string

var transportService =
Components.classes["@mozilla.org/network/socket-transport-service;1"].
getService(Components.interfaces.nsISocketTransportService);
var transport = transportService.createTransport(null,0,peerIpAddress,peerPort,null);

var listener = {
onHandshake : function ( context, reader ) {
// Received a valid handshake.
},
onRemoteId : function ( context, reader, remotePeerId ) {
// Received the remote peer id (20 byte identifier of remote peer)

},
onMessage : function( context, reader, messageId, messageBody ) {
// Good.
switch(message_id) {
// Keepalive doesn't have a message id, so we've used -1
case -1: alert("Keepalive"); break;
case 0: alert("Choke"); break;
case 1: alert("UnChoke"); break;
case 2: alert("Interested"); break;
case 3: alert("Not Interested"); break;
// etc.
default: alert("Message with id " + messageId);
}
// Message body is a string, representing the
// bytes in the message.
},
onDisconnect : function( context, reader ) {
// Disconnected
}
};
// Start reading from the input stream of the transport
reader.init(transport, ourId, remoteId, infoHash, listener, null /* context */);


More information on the wire protocol (Theory.org) Bittorrent Protocol Specification (BEP0003)
The bencoding XPCOM component.
The bittorrent tracker XPCOM component.
The Bittorrent File Info XPCOM component.
The Bittorrent File Storage XPCOM component.

Wednesday, July 14, 2010

Brick Wall - Memory Leak

Okay, I've reached a wall. Every time I do sockets, I end up with a memory leak.
Here's an example JS script that can be run with xpcshell (./run_mozilla.sh ./xpcshell pathToWhereYouSavedScript when you've built firefox).

The 'main' method for this program is testSockets(). It creates a server, and then connects to that server. We then see in testSockets_tick() that every 200 milliseconds we'll write more data out. We also see in the onDataAvailable() that the data is read, but not assigned to any variable. If this program is run, it chews and chews and chews memory.

I would assume that the inputstream/binaryinputstream is not holding the data, as calling readBytes() would release the data from the inputstream (and create a string, which would be soon freed by it's reference count being 0) - is this correct?

I would assume that the outputstream/binaryoutputstream is not holding the data, as it would be cleared after being sent - is this correct?

If both of the above are correct, where am I leaking?


/**************************
* This section is from mozilla/src/testing/xpcshell/head.js
* Licensed MPL 1.1/GPL 2.0/LGPL 2.1... as much as I'd like to spend half this post with a license,
* see http://hg.mozilla.org/mozilla-central/file/17dc041b9884/testing/xpcshell/head.js
*******************/
function do_timeout(delay, expr) {
var timer = Components.classes["@mozilla.org/timer;1"]
.createInstance(Components.interfaces.nsITimer);
timer.initWithCallback(new _TimerCallback(expr, timer), delay, timer.TYPE_ONE_SHOT);
}
var _pendingCallbacks=[];
function _TimerCallback(expr, timer) {
this._expr = expr;
// Keep timer alive until it fires
_pendingCallbacks.push(timer);
}
_TimerCallback.prototype = {
_expr: "",

QueryInterface: function(iid) {
if (iid.Equals(Components.interfaces.nsITimerCallback) ||
iid.Equals(Components.interfaces.nsISupports))
return this;

throw Components.results.NS_ERROR_NO_INTERFACE;
},

notify: function(timer) {
_pendingCallbacks.splice(_pendingCallbacks.indexOf(timer), 1);
eval(this._expr);
}
};
/**************************
* Code we're testing.
*********************/

var serverInfo;
var clientInfo;
var data;
function testSockets() {
// Create the data
data = (new Array(1024 * 20)).join("a");
// Create a server
serverInfo = startSocketServer(6503,function(result) {
serverInfo = result;
print("Started");
testSockets_tick();

});
clientInfo = connectToSocketServer("127.0.0.1",6503,function() {
// okay, disconnected.
print("Disconnected");
});
}
var ticks=0;
function testSockets_tick() {
// Every tick, send more data to client
serverInfo.sout.write(data,data.length);
ticks++;
do_timeout(200, "testSockets_tick();");
}

function connectToSocketServer(host,port,callback) {
var result = { sout : false, sin : false};
var transportService =
Components.classes["@mozilla.org/network/socket-transport-service;1"].
getService(Components.interfaces.nsISocketTransportService);
var transport = transportService.createTransport(null,0,host,port,null);

var outstream = transport.openOutputStream(0,0,0);
result.sout = outstream;
var stream = transport.openInputStream(0,0,0);
var instream = Components.classes["@mozilla.org/binaryinputstream;1"].
createInstance(Components.interfaces.nsIBinaryInputStream);
instream.setInputStream(stream);
result.sin = instream;

var dataListener = {
data : "",
onStartRequest: function(request, context){
},
onStopRequest: function(request, context, status){
instream.close();
outstream.close();
callback();
},
onDataAvailable: function(request, context, inputStream, offset, count){
// Not even going to save the data we read, let it go.
instream.readBytes(count);
}
};
var pump = Components.
classes["@mozilla.org/network/input-stream-pump;1"].
createInstance(Components.interfaces.nsIInputStreamPump);
pump.init(stream, -1, -1, 0, 0, false);
pump.asyncRead(dataListener,null);
return result;
}

function startSocketServer(port,readyCallback) {
// http://web.archive.org/web/20080313034101/www.xulplanet.com/tutorials/mozsdk/serverpush.php
if (port === undefined) {
port = 7055;
}
var result = {};
var listener = {
onSocketAccepted : function(serverSocket, transport) {
result.sout = transport.openOutputStream(0,0,0);
var instream = transport.openInputStream(0,0,0);
result.sin = Components.classes["@mozilla.org/binaryinputstream;1"].
createInstance(Components.interfaces.nsIBinaryInputStream);
result.sin.setInputStream(instream);

if (!(readyCallback === undefined)) {
readyCallback(result);
}
},
onStopListening : function(serverSocket, status){
print("Server shutdown");
}
};

result.serverSocket = Components.classes["@mozilla.org/network/server-socket;1"]
.createInstance(Components.interfaces.nsIServerSocket);
result.serverSocket.init(port,false,-1);
result.serverSocket.asyncListen(listener);
return result;
}

testSockets();

/**************
* Code to force xpcshell NOT to quit, allowing async processing
* From https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO
***************/
gScriptDone = false;
var gThreadManager = Components.classes["@mozilla.org/thread-manager;1"]
.getService(Components.interfaces.nsIThreadManager);
var mainThread = gThreadManager.currentThread;

while (!gScriptDone)
mainThread.processNextEvent(true);
while (mainThread.hasPendingEvents())
mainThread.processNextEvent(true);




Update: I've separated out the code into a script that runs the server, and a script that runs the client. By watching the process id, I can see that it's the client that is having the problem. nsIBinaryInputStream perhaps. So, I kept trying a few things, changing the method I was using. I tried calling gc(); at different times - this worked, but I was getting some strange results depending on how often I was calling it. I used if(count++>20000) { count=0; gc(); } in the method that received data; this worked well, but if I changed it to if(count++==20000) it didn't work at all... bizarre. I've now moved the gc(); call into it's own function that is called every 4 seconds and it's going okay. This is fantastic to me. This could've been what was causing my issues a year ago, and to finally be rid of it... could this be true? Hmm... well, this debug session has left me tired and drained (it's 7:45AM), but I think it's left my code in a better state than I am.