Monday, July 12, 2010

Info - File Storage

If you've used a Bittorrent client, or maybe have installed many games using Steam, you'll notice that files are preallocated space before the download begins. This is different from your average HTTP (web browser) download, though many HTTP clients could do this - there's just less reason for it.
With Bittorrent the download will tend to occur by acquiring pieces of files not in sequential order. This isn't always the case, and some extensions to Bittorrent may mean it's easier to stream the file in the original order. Regardless, it's the norm for the files to be created and then parts overwritten as the file's real contents are received.

Preallocating
My notes reference DownThemAll for when the preallocation code was written - though pre-allocation in downThemAll is different, the general approach is the same: use the nsISeekableStream to get to the position where the last byte would be.

Inserting/Extracting
MDC's guide to writing binary data is excellent, though I've previously had trouble with the suggested file permissions (resulting in (NS_ERROR_FILE_ACCESS_DENIED) [nsIFileOutputStream.init] ). I've reached success through trial and error, though I should probably revisit this at sometime.
The extract method has the form extract(in nsILocalFile baseFolder, in nsIVariant pathArray, in long offset, in long datalength) returning an ACString. Path arrays in Bittorrent have been discussed previously; they represent the path to the file by splitting the path into several strings instead of relying on a OS dependent directory delimiter. The BaseFolder parameter allows an easy way of setting the location where the torrent will be downloaded without having to alter every pathArray.
I still use nsIBinaryInputStream with the readBytes function, which is why the ACString type is being used.

File Storage
So yes, all of this focuses on storing the data in files. It doesn't have to be that way, a torrent can be held completely in memory for other uses... I hope to be able to write about some fun with Bittorrent that is less concerned with just implementing the protocol. Here's Brad Goldsmith's CompTorrent for some interesting use of bittorrent. He has since submitted his PhD thesis, so there is further reading if you're interested.

Anyway, onto files:
Implementation of FileStorage XPCOM component
IDL for FileStorage XPCOM component

Examples:

Usage:

var service = Components.
classes["@wikiscraps.com/storage/file;1"].
getService(Components.interfaces.btIBittorrentFileStorage);
var baseFolder = Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties).
get("TmpD", Components.interfaces.nsILocalFile);
baseFolder.appendRelativePath("test");
// We'll create two files, one in a subfolder
var list = [{path:["a.txt"],size:10},{path:["a","a.txt"],size:10}];
service.preallocate(baseFolder,list);
service.insert(baseFolder, ["a.txt"], 6, "cdef");
service.insert(baseFolder, ["a.txt"], 4, "abcd");
var val = service.extract(baseFolder, ["a.txt"], 4, 6); // returns "abcdef"


Usage with FileInfo:
var infoService = Components.
classes["@wikiscraps.com/fileinfo;1"].
getService(Components.interfaces.btIBittorrentFileInfo);
var storageService = Components.
classes["@wikiscraps.com/storage/file;1"].
getService(Components.interfaces.btIBittorrentFileStorage);
var baseFolder = Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties).
get("TmpD", Components.interfaces.nsILocalFile);
baseFolder.appendRelativePath("test");
// Fake block
var fakeBlock = "Baaaaaaaaaaa";
var fakeBlockOffset = 5;
// Fake info
var fakeInfo = {
files : [
{ length : 10, path : ["a"] },
{ length : 10, path : ["b"] },
{ length : 10, path : ["c"] },
{ length : 10, path : ["d"] },
{ length : 10, path : ["e"] }
]
};
// Add piece length later, because it has a space in it's key.
fakeInfo["piece length"] = 6;

// Okay, we've faked the state, now
// let's try to save the block to files!

var fileList = infoService.listFile(fakeInfo);
var fileParts = infoService.blockToFileParts(fakeInfo, fakeBlockOffset, fakeBlock.length);
var completedIndex = 0;
for (var i=0; fileParts.length > i; i++) {
storageService.insert(
baseFolder,
fileList[fileParts[i].index],
fileParts[i].offset,
fakeBlock.substr(completedIndex,fileParts[i].size));
completedIndex += fileParts[i].size;
}


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

Saturday, July 10, 2010

File Storage for torrent

Writing received data from bittorrent is a bit of a pain. As previously described, a block may end up split into a number of files.
When I first did this, my code for handling a received block was complex. The task of splitting the block into parts relevant for separate files was mixed with the task of writing to files. A tad harder to read and debug.
Now, it's possible to cleanly work out the file parts and write to them. The file storage component is really simple, with just 3 methods: preallocate, insert & extract. I'll post the code tomorrow, for now here's a demo of how it's used with the FileInfo component:


var infoService = Components.
classes["@wikiscraps.com/fileinfo;1"].
getService(Components.interfaces.btIBittorrentFileInfo);
var storageService = Components.
classes["@wikiscraps.com/storage/file;1"].
getService(Components.interfaces.btIBittorrentFileStorage);
var baseFolder = Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties).
get("TmpD", Components.interfaces.nsILocalFile);
baseFolder.appendRelativePath("test");
// Fake block
var fakeBlock = "Baaaaaaaaaaa";
var fakeBlockOffset = 5;
// Fake info
var fakeInfo = {
files : [
{ length : 10, path : ["a"] },
{ length : 10, path : ["b"] },
{ length : 10, path : ["c"] },
{ length : 10, path : ["d"] },
{ length : 10, path : ["e"] }
]
};
// Add piece length later, because it has a space in it's key.
fakeInfo["piece length"] = 6;

// Okay, we've faked the state, now
// let's try to save the block to files!

var fileList = infoService.listFile(fakeInfo);
var fileParts = infoService.blockToFileParts(fakeInfo, fakeBlockOffset, fakeBlock.length);
var completedIndex = 0;
for (var i=0; i < fileParts.length; i++) {
storageService.insert(
baseFolder,
fileList[fileParts[i].index],
fileParts[i].offset,
fakeBlock.substr(completedIndex,fileParts[i].size));
completedIndex += fileParts[i].size;
}

Fairly clear, and much easier to run tests on. A few things ahead bother me. Wrapping the nsIBinaryInputStream to handle the Bittorrent wire protocol should be easy, though I can see foresee that handling the received messages will probably end up with a suboptimal structure. Writing the code to order block requests will be a pain, and I will thankfully separate that into it's own component.

Friday, July 9, 2010

Info - File Information in Bittorrent

The information about files is stored in the info section of the .torrent file. There is a slight difference in this structure if the torrent contains one file or whether it contains more than one file. To make handling some of that information a tad easier, I have a collection of methods for common tasks and to hide the single/multiple file differences.

Subtle Differences
The structure of the info data is different depending on whether it's a torrent containing one file or many. This makes for a large number of "if" conditions when handling the info data, and it's just easier if this is hidden away. The old Firepuddle Firefox/Bittorrent extension didn't get around to handling multiple files; the info data is just the tip of the iceberg of pain that multiple file torrents can be.

Common Functions
The methods the service I've written provides are fairly simple; though partly that's to keep the handling of different info data structures in one place. listFiles(in nsIVariant decodedInfo) returns an array of file paths. A single file has its path stored in a torrent file as an array, for example ["foldername","foldername","filename"] which avoids the issue of an OS specific directory separator character. Handy. So, that means the result of listFiles is an array of arrays of strings. fileCount(in nsIVariant decodedInfo) returns the number of files. fileSize(in nsIVariant decodedInfo, in long fileIndex) returns the size in bytes of a file, given an index from the list. I should note, the ordering of the listFiles function is that provided by the info data.

Not so necessary functions
A couple of functions I don't believe I've used for handling the downloading/uploading of a torrent, but I've needed for another application of bittorrent are fileInTorrent(in nsIVariant decodedInfo, in nsIVariant filePathArray) returning a boolean result (the file path array is expected to be an array of strings, as described previously), and findFile(in nsIVariant decodedInfo, in nsIVariant filePathArray) to provide the index in the fileList of a particular file path. -1 if not found, as normal.

Tetris Block functions
During a session the content of torrent is transferred as a series of blocks, in no pre-ordained order. These blocks are some subsection of the torrent, and one block may end up split into multiple files.
To figure out where a block belongs, blockToFileParts(in nsIVariant decodedInfo, in long offset, in long blockLength) returns an array of objects. Each object specifies a fileIndex, the offset in the file, and how many bytes from the offset belong to that file. The structure of this returned data is like so: [{index:...,offset:...,size:...},...].
The torrent is split into a series of pieces that have a checksum. Sometimes not all files in a torrent are desired, so not all pieces will be needed. To find out which pieces are needed for a particular file piecesInFile(in nsIVariant decodedInfo, in long fileIndex) will return an array of numbers, each number representing a piece index necessary for the file specified by the fileIndex parameter.

Implementation of FileInfo XPCOM component
IDL for FileInfo XPCOM component

Example:

var service = Components.
classes["@wikiscraps.com/fileinfo;1"].
getService(Components.interfaces.btIBittorrentFileInfo);
var fakeInfo = {
files : [
{ length : 10, path : ["a"] },
{ length : 10, path : ["b"] },
{ length : 10, path : ["c"] },
{ length : 10, path : ["d"] },
{ length : 10, path : ["e"] }
]
};
// Add piece length later, because it has a space in it's key.
fakeInfo["piece length"] = 6;
// Check service methods
// Check the list files/file count/file size/file in torrent/find file
var fileList = service.listFiles(fakeInfo);
service.fileCount(fakeInfo); // 5
service.fileSize(fakeInfo, 0); // 10
service.fileInTorrent(fakeInfo, ["a"]); // this will return true
service.fileInTorrent(fakeInfo, ["z"]); // this will return false
service.findFile(fakeInfo, ["c"]); // this will return 2
service.findFile(fakeInfo, ["z"]); // this will return -1 (not in torrent)
service.blockToFileParts(fakeInfo, 10, 9); // returns [{index:1,offset:0,size:9}]
service.piecesInFile(fakeInfo, 1); // returns [1,2,3] (pieces are indexed from 0)

Note that none of this code deals with the relationship between pieces and hashes. At the moment I don't think that's needed in this service, though I might consider a verify piece against hash method at some point. This service will probably change by the time I've finished refactoring.

More information on the .torrent structure (Theory.org) Bittorrent Protocol Specification (BEP0003)
The bencoding XPCOM component.
The bittorrent tracker XPCOM component.

Thursday, July 8, 2010

XPCOM Generating SHA1 hash on a string

Reading some old code, thought this would be interesting. I tend to comment a fair bit, particularly when I'm frustrated or think I'll forget something. What follows are my comments from December 2008, and some updated SHA1 code.
/*****
* Generate a SHA1 hash of some data.
* Sources:
* Mainly based on example from https://developer.mozilla.org/en/NsICryptoHash
* With a conversion from string to array of numbers from http://www.cozmixng.org/retro/projects/piro/diff/backforwardthumbnail/trunk/content/backforwardthumbnail/backforwardthumbnail.js?compare_with=1438&format=diff&rev=1440
*
* Discussion:
* The basis of this function is the Mozilla documentation for nsICryptoHash,
* which does include a demonstration of generating a hash for a string -
* but it won't work (at least in this case). The Mozilla docs do some
* Unicode handling, however this generates an incorrect hash for our
* purposes. Instead, I found if I saved the data to a file, and then
* opened that file and passed the stream to nsICryptoHash, I would get the
* correct result. So, ignore the Unicode handling. Saving then opening a
* file is crazy, so a different stream was needed. StringInputStream was
* considered, and is used by FastServicesJavaScript (1) and Flock (2),
* but XUL Planet docs mention that the incoming string shouldn't contain
* null. This condition can't be guaranteed with the data we're handling,
* so another option was needed. The update method for nsICryptoHash can
* take an array of bytes. How can we convert a string to an array of
* bytes? Apparently, easily. Just create an array of the character's
* ordinal value using charCodeAt. This technique was found from
* cozmixng (3), and is a cut-and-paste of 3 lines.
* 1 - 2008/DEC http://wiki.fastmail.fm/index.php?title=FastServicesJavascript
* 2 - 2008/DEC https://lists.flock.com/pipermail/svn-commits/2007-September/012334.html
* 3 - 2008/DEC http://www.cozmixng.org/retro/projects/piro/diff/backforwardthumbnail/trunk/content/backforwardthumbnail/backforwardthumbnail.js?compare_with=1438&format=diff&rev=1440
* (3) is licensed under MPL, GPL & LGPL
* http://www.cozmixng.org/retro/projects/piro/browse/backforwardthumbnail/trunk/content/backforwardthumbnail/license.txt?rev=1429
********/

SHA1 = function (data) {
var hash = Components.classes["@mozilla.org/security/hash;1"]
.createInstance(Components.interfaces.nsICryptoHash);
hash.init(hash.SHA1);
// cozmixng split/map call
var byteArray = data.split('').map(function(c) { return c.charCodeAt(0); });
hash.update(byteArray, byteArray.length);
return hash.finish(false);
}

It's interesting re-reading this. I have found XPCOM components to be weird beasts, particularly when used in JavaScript. Looking back, this is partly from lack of understanding. I find understanding anything Mozilla will provide a headache; confirming a bug for example. It seems to me that once someone comes to understand anything in Mozilla, it is best if they blurt out that knowledge lest it be lost.

I really miss XUL Planet's documentation on sockets, that good old Pushing and Pulling. I feel lucky to have been able to catch that, and read their XPCOM reference before XUL Planet reached a decision to drop the content.

A few explanations on the SHA1 function, if this helps others. I tend to read data from sockets, and use nsIBinaryInputStream to do it. I don't use nsIScriptableInputStream, in fact I will probably never again use nsIScriptableInputStream simply because it doesn't handle null values. In the past, I've used the readBytes method (of nsIBinaryInputStream) which provides the result as a string in JavaScript. This may have been a good idea, maybe a bad one. Regardless, it is the reason for the above need to call split() on a string. The string is really just a collection of bytes, but now it's in JavaScript so everything has become a little odd, so we need to change the appearance of the string into an array of numbers (split to an array, map to numbers), and then let XPCOM handle the conversion so nsICryptoHash receives an array of bytes.

It seems so bloody obvious now, but reading the comments above replays part of the journey it took to get there.

Wednesday, July 7, 2010

Liking getService over createInstance (plus some xpcshell unit tests for bittorrent extension)

So, I've finished the tracker code as a service, instead of creating an object for each tracker we communicate with. I like it. Next, I'm writing a service to simplify interpreting the info section of a .torrent file (once this data has been bedecoded). Actually, the info section is really easy to understand, as seen in Theory.org's Bittorrent Protocol documentation. It's just that the common things I want to know are not always easy to infer directly from the info section.

If we receive a block 100 bytes long that belongs at offset 3,000 in the torrent, what part of which files does this represent? I like to have a function that given the offset and block length will convert this to an array of objects each containing the filename, offset in the file, and how many bytes to write. You know? Something like blockToFileParts(decodedInfo,offset,blockLength) returning [{file:...,offset:...,size:...},...]. Now I can start taking chunks from the received block and placing them in the correct places in files.

Sooo, that's what's next. I've been testing it with a bunch of unit tests, which have proven quite valuable. I've caught several bugs in a controlled situation, which is much easier than debugging using a network sniffer and the good old pen and paper calculator. Here's a peak at the tests, I'll see about dropping the component tomorrow. It's obviously very much a specialised utility class, it doesn't appear obviously useful to anyone but myself at this time.


function fileInfoTests(){
do_check_eq("Now testing fileInfo", "Now testing fileInfo");
// Check class loaded
do_check_true("@wikiscraps.com/fileinfo;1" in Components.classes);
// Check interface exists
do_check_true("btIBittorrentFileInfo" in Components.interfaces);
// Grab the service object.
var service = Components.
classes["@wikiscraps.com/fileinfo;1"].
getService(Components.interfaces.btIBittorrentFileInfo);
var fakeInfo = {
files : [
{ length : 10, path : ["a"] },
{ length : 10, path : ["b"] },
{ length : 10, path : ["c"] },
{ length : 10, path : ["d"] },
{ length : 10, path : ["e"] }
]
};
// Add piece length later, because it has a space in it's key.
fakeInfo["piece length"] = 6;
// Check service methods
// Check the list files/file count/file size/file in torrent/find file
var fileList = service.listFiles(fakeInfo);
do_check_eq(fileList.length,5);
do_check_eq(service.fileCount(fakeInfo),5);
do_check_eq(service.fileSize(fakeInfo, 0),10);
do_check_true(service.fileInTorrent(fakeInfo, ["a"]));
do_check_false(service.fileInTorrent(fakeInfo, ["a","a"]));
do_check_false(service.fileInTorrent(fakeInfo, ["z"]));
do_check_eq(service.findFile(fakeInfo, ["a"]),0);
do_check_eq(service.findFile(fakeInfo, ["a","a"]),-1);
do_check_eq(service.findFile(fakeInfo, ["z"]),-1);
// Check the blockToFileParts method
// For a block spanning all of one file
var fileParts = service.blockToFileParts(fakeInfo, 0, 10);
do_check_eq(fileParts.length, 1);
do_check_eq(fileParts[0].index, 0);
do_check_eq(fileParts[0].offset, 0);
do_check_eq(fileParts[0].size, 10);
// For a block spanning two files
fileParts = service.blockToFileParts(fakeInfo, 0, 11);
do_check_eq(fileParts.length, 2);
do_check_eq(fileParts[0].index, 0);
do_check_eq(fileParts[0].offset, 0);
do_check_eq(fileParts[0].size, 10);
do_check_eq(fileParts[1].index, 1);
do_check_eq(fileParts[1].offset, 0);
do_check_eq(fileParts[1].size, 1);
// For a block of one byte near the beginning of a file
fileParts = service.blockToFileParts(fakeInfo, 10, 1);
do_check_eq(fileParts.length, 1);
do_check_eq(fileParts[0].index, 1);
do_check_eq(fileParts[0].offset, 0);
do_check_eq(fileParts[0].size, 1);
// For a block of one byte near the end of a file
fileParts = service.blockToFileParts(fakeInfo, 9, 1);
do_check_eq(fileParts.length, 1);
do_check_eq(fileParts[0].index, 0);
do_check_eq(fileParts[0].offset, 9);
do_check_eq(fileParts[0].size, 1);

// PieceList check
// With a pieceLength of 6 & file sizes of 10:
// File 0 contains piece 0 & 1 (4 bytes)
// File 1 contains pieces 1 (2 bytes), 2, 3 (2 bytes)
// File 2 contains pieces 3 (4 bytes), 4
// File 3 contains pieces 5, 6 (4 bytes)
// File 4 contains pieces 6 (2 bytes), 7, 8 (2 bytes, irregular sized piece)
var pieceList = service.piecesInFile(fakeInfo, 0);
do_check_eq(pieceList.length, 2);
do_check_eq(pieceList[0], 0);
do_check_eq(pieceList[1], 1);
pieceList = service.piecesInFile(fakeInfo, 1);
do_check_eq(pieceList.length, 3);
do_check_eq(pieceList[0], 1);
do_check_eq(pieceList[1], 2);
do_check_eq(pieceList[2], 3);
/* check pieceIndex 4 (5th piece) is correctly allocated to fileIndex 2 (3rd file) */
pieceList = service.piecesInFile(fakeInfo, 2);
do_check_eq(pieceList.length, 2);
do_check_eq(pieceList[1], 4);
pieceList = service.piecesInFile(fakeInfo, 3);
do_check_eq(pieceList.length, 2);
do_check_eq(pieceList[0], 5);
// Tests missing:
// when piece is shared between 3 files (piecesize > middle file size)
// Files at end of torrent
}

Hmm. Okay, so a few of those methods aren't clear without understanding what the parameters mean. Here's the IDL:


[scriptable, uuid(463206c3-74dd-44f4-b6bf-dc8fbc198993)]
interface btIBittorrentFileInfo : nsISupports
{
nsIVariant listFiles(in nsIVariant decodedInfo);
long fileCount(in nsIVariant decodedInfo);
long fileSize(in nsIVariant decodedInfo, in long fileIndex);
boolean fileInTorrent(in nsIVariant decodedInfo, in nsIVariant filePathArray);
long findFile(in nsIVariant decodedInfo, in nsIVariant filePathArray);
nsIVariant blockToFileParts(in nsIVariant decodedInfo, in long offset, in long blockLength);
nsIVariant piecesInFile(in nsIVariant decodedInfo, in long fileIndex);
};


This again isn't completely useful, because I've used nsIVariant where arrays or objects would be. Ugh... I can see documentation would be shorter way to enlightenment.

Tuesday, July 6, 2010

Info - Bittorrent Tracker (and XPCOM component)

The tracker is a web service that when given an id of a torrent will respond with a list of IP addresses/ports of other peers interested in that torrent.

Torrent Identity
The identity of a torrent (the 'infoHash') is generated from a hash using SHA1 on the section of the .torrent file describing the list of files & checksums. This means that the rest of the .torrent file can be edited, while the torrent identity will remain unchanged. The editing may be the addition or removal of a tracker for the torrent, or some other meta data that itself is not the 'contents' of the torrent.

Finding Trackers
The .torrent file will contain at least one tracker, identified as 'announce'. Additional trackers may appear in the .torrent file as 'announce-list'.

Talking to Trackers
Trackers are given a HTTP GET request with information passed via the query string. The payload in the response is usually a bencoded dictionary listing other peers in the torrent, though this is an optimal response. Sometimes it's a string describing an error such as not accepting requests without compact=1 (the format of the peers list may be a compact string, or a list of dictionaries; the component defaults to compact=1 to avoid this issue). Darn.

Talking to Trackers, again
A subsequent call to a tracker may occur, to get more peers or to update the tracker on progress. The original tracker response may have included a minimum timeout to use before making this follow up call, and also a Tracker Id which should be sent as part of the follow up call.


Implementation of Tracker XPCOM component
IDL for Tracker XPCOM component

Example:
var trackerCaller = Components.
classes["@wikiscraps.com/tracker;1"].
getService(Components.interfaces.btIBittorrentTracker);
// Get a list of peers
trackerCaller.start(
// infohash of http://www.mininova.org/det/3194273
"\x72\xc2\x4e\x55\xc3\x2c\xde\xf7\x23\x78\x12\xf1\x69\x4d\xad\xd1\xe7\x13\x19\x97",
// announceURI
"http://tracker.mininova.org/announce",
"", // tracker Id
"-YY0000-000000000000", // peer id (should've generated & recorded this)
6881, // local bittorrent port
-1, // maximumPeers (-1 means use default)
0, // uploaded bytes
0, // downloaded bytes
0, // remaining bytes...
{
onResponse : function(peers, interval, trackerId) {
// got a list of peers
alert("Look! Peers!\n" +peers);
// the interval before another request to the tracker can be made
alert
("Timeout (in seconds) before next tracker request:\n" +interval);
// the id to use for subsequent calls to the tracker
alert("Tracker's Id:\n" + trackerId);
},
onError : function(serverResponse) {
// darn.
alert("Unknown error, tracker component response was:\n" + serverResponse);
}
});

More information on the .torrent structure (Theory.org) Bittorrent Protocol Specification (BEP0003)
More information on the tracker response (Theory.org) Bittorrent Protocol Spec (BEP0003)
The bencoding XPCOM component.

Monday, July 5, 2010

Leak due to XPCOM service held in global variable

I've been running some xpcshell tests. I'm testing the bittorrent tracker code, which is asynchronous so that's kinda fun. It means that I call do_test_pending(); before starting the tracker request. Oh, let's show a test:

function run_test() {
// Just check we're running tests okay
do_check_true(true);
// Check the interface has been loaded correctly
do_check_true("btIBittorrentTracker" in Components.interfaces);
// Check the component has been registered correctly
do_check_true("@wikiscraps.com/tracker;1" in Components.classes);
// Grab the service
var trackerService = Components.
classes["@wikiscraps.com/tracker;1"].
getService(Components.interfaces.btIBittorrentTracker);
do_test_pending();
// Make a request to the tracker
trackerService.start(
// infohash of http://www.mininova.org/det/3194273
"\x72\xc2\x4e\x55\xc3\x2c\xde\xf7\x23\x78\x12\xf1\x69\x4d\xad\xd1\xe7\x13\x19\x97",
// announceURI
"http://tracker.mininova.org/announce",
"", // tracker Id
// peer id (should've generated & recorded this)
"-YY0000-000003030303",
6881, // local bittorrent port
-1, // maximumPeers (-1 means use default)
0, // uploaded bytes
0, // downloaded bytes
0, // remaining bytes...
{
onResponse : function(peers, interval, trackerId) {
// got a list of peers
do_check_true(peers instanceof Array);
// finished tests
do_test_finished();
},
onError : function(serverResponse) {
// darn. Fail the test.
do_throw("Unknown error, server response was:\n" + serverResponse);
do_test_finished();
}
});
}

So you pass the tracker service the information needed to make a call to the tracker, and some kind of observer/callback to use when there's a result known. Okay? Got it? Cool.
Anyways, I started seeing a BloatView report after running this test. It didn't appear originally, so I assume it only appears when some kind of leak occurs.

== BloatView: ALL (cumulative) LEAK STATISTICS

|<----------------Class--------------->|<-----Bytes------>|<----------------Objects---------------->|<--------------References-------------->|
Per-Inst Leaked Total Rem Mean StdDev Total Rem Mean StdDev
0 TOTAL 27 520 24783 11 ( 183.34 +/- 238.02) 80546 13 ( 263.14 +/- 337.35)
2 BackstagePass 24 24 1 1 ( 1.00 +/- 0.00) 6366 4 ( 148.79 +/- 78.75)
19 XPCNativeScriptableShared 112 112 1053 1 ( 12.21 +/- 1.38) 0 0 ( 0.00 +/- 0.00)
22 XPCWrappedNative 56 168 1322 3 ( 661.75 +/- 381.27) 8044 3 ( 682.46 +/- 383.57)
23 XPCWrappedNativeProto 32 64 519 2 ( 260.00 +/- 149.61) 0 0 ( 0.00 +/- 0.00)
100 nsJSID 36 36 116 1 ( 58.25 +/- 33.42) 499 1 ( 117.51 +/- 67.79)
135 nsStringBuffer 8 8 2600 1 ( 435.41 +/- 266.14) 4832 1 ( 413.00 +/- 254.37)
144 nsSystemPrincipal 36 36 1 1 ( 1.00 +/- 0.00) 347 1 ( 4.72 +/- 1.09)
147 nsThread 72 72 3 1 ( 1.80 +/- 0.84) 4598 3 ( 571.22 +/- 379.54)

nsTraceRefcntImpl::DumpStatistics: 180 entries

Ouch. So, going back over what I'd done recently, I found this:

if (Tracker.prototype._bencoding == false) {
try {
Tracker.prototype._bencoding = Components.
classes["@wikiscraps.com/encoding/bencoding;1"].
getService(Components.interfaces.Bencoding);
} catch(e) {
callback.onError("Bencoding component not loaded");
return;
}
}

It seems loading a XPCOM service, and then leaving it in a 'global' variable and never explicitly releasing it caused a leak. Whoops. The BloatView goes away if I switch to using local variables and acquire the service each time it's needed, so it seems I can call that fixed?

More information on testing a JavaScript Add-on with XPCShell (Interfaces and components!).
More information on writing XPCShell unit tests.