Refactor for testing of Syncbase debug viewer.

Previously there was no testing of the Syncbase debug viewer code.  This
CL refactors out the non-UI code into a separate package `sbtree` and
adds tests for that code.

Also added to syncbase/testutil a CreateSyncgroup function.

These tests found a wrong-index-variable bug bug similar to the one that
Fred previously found.  This Cl aslo fixes that bug.

Change-Id: If273e4035b71fdc3734e52b98e964ff49b5d6365
diff --git a/services/debug/debug/browseserver/assets.go b/services/debug/debug/browseserver/assets.go
index a26d861..0172e73 100644
--- a/services/debug/debug/browseserver/assets.go
+++ b/services/debug/debug/browseserver/assets.go
@@ -143,7 +143,7 @@
 	return a, nil
 }
 
-var _collectionHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x55\x51\x6f\xd3\x30\x10\x7e\xef\xaf\x38\x55\xa8\x6c\x93\x92\xb0\x21\x78\x80\x34\x68\xeb\x26\x31\x81\xaa\x89\x8e\xbd\x20\x1e\x9c\xf8\xd2\x58\x73\xed\xca\x76\xcb\x4a\xd4\xff\xce\x39\x49\x69\x13\xd2\x89\x27\x24\x22\xb5\xf5\x7d\x77\xdf\x9d\x7d\xf7\xd5\x29\xcb\xe8\x6c\x00\xf4\x4c\xf4\x72\x63\xc4\xbc\x70\x70\xf1\xea\xfc\x2d\xdc\x17\x08\x0f\x4c\x31\x2e\x56\x0b\xb8\x5c\xb9\x42\x1b\x1b\xc2\xa5\x94\x50\x05\x59\x30\x68\xd1\xac\x91\x87\x15\xfb\xab\x45\xd0\x39\xb8\x42\x58\xb0\x7a\x65\x32\x84\x4c\x73\x04\x32\xe7\x7a\x8d\x46\x21\x87\x74\x03\x0c\xae\x66\xd7\x81\x75\x1b\x89\x15\x4d\x8a\x0c\x15\x51\x5d\xc1\x1c\x64\x4c\x41\x8a\x90\xeb\x95\xe2\x20\x14\x81\x08\x9f\x6f\x27\x37\xd3\xd9\x0d\xe4\x42\x62\x38\x38\x8b\xb6\xdb\xc1\xa0\x2c\x39\xe6\x42\x21\x0c\x33\xad\x1c\x2a\x37\xf4\x68\x6c\x31\x73\x42\x2b\xc8\x24\xb3\x76\x3c\x6c\xcc\x20\xa0\x0a\x0e\x0d\x2c\xb8\x0c\xe6\x46\xf0\x61\x52\x55\x8e\x8b\x37\xc9\x44\x4b\xd9\x90\xca\x32\xbc\x66\x8e\xa5\xcc\x62\x78\xcb\xc3\x29\x5b\xe0\x76\x1b\x11\xba\x8f\xd9\xe3\x71\x44\xe4\x3a\x0b\x17\xeb\x5d\x41\x5f\x20\x43\x6a\xd0\x6e\x11\x04\xe7\x17\x41\xa6\x65\x53\xb1\x8e\x97\x7b\xc3\x9b\x2e\xb9\x5c\x2e\xe1\x4a\xa2\xb5\x42\xcd\xdf\x79\x20\xe6\x3c\xe9\x6c\x67\xe7\xf7\xa5\xc9\xdb\xc9\x30\x31\xc8\x9c\x36\x2f\xad\x9f\x82\xe9\x4d\xd6\x3e\xc5\xf3\xe9\xa6\xab\x45\x4a\x69\x68\x9a\x5f\xf4\x0f\x7b\x98\x85\xec\x09\x0d\xc7\xf5\xf3\xee\xb5\x63\x12\x3e\xe1\x06\x66\xe2\x27\x1e\xf2\xc8\x43\xb0\x47\xfb\x99\xe4\xb4\xfe\xb7\x05\xb7\xa2\x08\xa0\x6e\x48\x6c\x63\x1e\x35\x49\xec\x8a\x24\x8e\xfc\x17\x7d\x28\xd5\xef\xf5\x03\x93\x2b\xac\xad\x88\xe2\xfa\xa9\x24\x53\x69\x97\x4c\x8d\x87\xaf\x87\xdd\x10\x20\x59\x88\x1c\x42\xbf\xbf\x3b\x36\xc7\xf0\x23\xb3\x77\x06\xd7\x24\xb7\x6e\x20\xe5\x63\x50\x18\xcc\xc7\x24\xca\x5d\xaf\x3f\xa8\x71\x59\xbe\x08\x67\xfe\x7f\x62\x6a\xe9\x8c\x78\x3a\x3e\x3a\xdc\x11\x57\x5d\x67\xc3\xca\x2a\xd6\xb1\x29\x8e\x32\xf5\xa7\xbb\x66\xf6\x9c\xc9\x3f\xdf\x52\x96\x3d\x7e\xef\x3b\x45\xc4\xfa\xba\x80\x8a\xf7\x1c\x7a\xa4\x52\xbb\x7c\xdf\xed\xeb\xb1\x86\x97\xa5\x61\x6a\x8e\x07\xed\xa4\x05\xcd\xc8\xf6\x64\xae\x87\x53\x89\xe7\x56\x71\x7c\xf2\xba\x21\xb3\x81\x88\x77\x00\xc4\xfe\xa2\xf1\x70\x35\x6f\xef\xa8\x80\xda\xdf\xbb\x0f\x94\x16\x8f\x17\x6d\x29\xe2\x44\x69\x78\xa4\xfd\x9e\x3e\x97\xae\xa7\x39\x7f\xaf\x2e\x89\xea\xa0\x25\x53\x7c\x72\xd5\xf1\xfe\x1b\x85\x8d\x72\x61\xac\xa3\x1e\x8d\xeb\xc9\x74\xce\x71\x44\x80\x27\x8a\x02\x4e\xff\xa5\x00\x09\xe9\xde\x20\xed\xbb\x88\xac\xe6\x72\xa6\x95\x58\x27\x83\x38\x6a\xde\x21\x89\x7f\xe7\xd4\x3b\xf8\x15\x00\x00\xff\xff\xd3\x91\x27\x27\x2f\x07\x00\x00")
+var _collectionHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x55\xd1\x6f\xda\x3e\x10\x7e\xe7\xaf\x38\xa1\x9f\xf8\xb5\x95\x92\xac\x9d\xb6\x87\x2d\x64\x6a\x69\xa5\x55\x9b\x50\x35\xba\xbe\x4c\x7b\x70\xe2\x0b\xb1\x6a\x6c\x64\x1b\x56\x16\xf1\xbf\xef\x9c\x84\x32\xd2\x04\xed\x69\xd2\x22\x01\xbe\xef\xbe\xbb\xb3\xef\x3e\x9c\xb2\x8c\xce\x06\x40\xcf\x44\x2f\x37\x46\xcc\x0b\x07\x17\xaf\xce\xdf\xc2\x7d\x81\xf0\xc0\x14\xe3\x62\xb5\x80\xcb\x95\x2b\xb4\xb1\x21\x5c\x4a\x09\x15\xc9\x82\x41\x8b\x66\x8d\x3c\xac\xa2\xbf\x5a\x04\x9d\x83\x2b\x84\x05\xab\x57\x26\x43\xc8\x34\x47\x20\x73\xae\xd7\x68\x14\x72\x48\x37\xc0\xe0\x6a\x76\x1d\x58\xb7\x91\x58\x85\x49\x91\xa1\xa2\x50\x57\x30\x07\x19\x53\x90\x22\xe4\x7a\xa5\x38\x08\x45\x20\xc2\xe7\xdb\xc9\xcd\x74\x76\x03\xb9\x90\x18\x0e\xce\xa2\xed\x76\x30\x28\x4b\x8e\xb9\x50\x08\xc3\x4c\x2b\x87\xca\x0d\x3d\x1a\x5b\xcc\x9c\xd0\x0a\x32\xc9\xac\x1d\x0f\x1b\x33\x08\xa8\x82\x43\x03\x0b\x2e\x83\xb9\x11\x7c\x98\x54\x95\xe3\xe2\x4d\x32\xd1\x52\x36\x41\x65\x19\xde\x1b\xc4\xf0\x9a\x39\x96\x32\x8b\xe1\x2d\x0f\xa7\x6c\x81\xdb\x6d\xb4\x73\xed\xd9\x7b\x67\x1c\x51\x9a\x3a\x1f\x17\xeb\x5d\x69\x5f\x2a\x43\x6a\xd5\x6e\x11\x04\xe7\x17\x41\xa6\x65\x53\xbb\xe6\xcb\xbd\xe1\x4d\x97\x5c\x2e\x97\x70\x25\xd1\x5a\xa1\xe6\xef\x3c\x10\x73\x9e\x74\x6d\x6c\x47\xf2\xf5\x89\xd2\x4a\x33\x31\xc8\x9c\x36\xff\x5b\x3f\x14\xd3\x9f\xf1\xf0\x3c\xc7\x73\x4e\x57\x8b\x94\x72\xd1\x84\xbf\xe8\x1f\xf6\x45\x2a\x02\x27\x34\x35\xd7\x1d\x7c\xaf\x1d\x93\xf0\x09\x37\x30\x13\x3f\xf1\x45\x30\xb9\xc9\xe7\x5d\xdd\xe1\xe4\xb4\xfe\xf7\x00\x3e\x60\x11\x40\xcd\x91\x78\x88\x79\xd4\x24\xb1\x2b\x92\x38\xf2\x5f\xf4\xa1\x54\xcf\xeb\x07\x26\x57\x58\x5b\x11\xf1\xba\x43\x49\xc4\xd2\x2e\x99\x1a\x0f\x5f\x0f\xdb\x14\x20\xd1\x88\x1c\xea\x43\xf8\x4d\xde\xb1\x39\x86\x1f\x99\xbd\x33\xb8\x26\x45\xb6\xd9\x94\x94\x41\x61\x30\x1f\x93\x6e\x77\xad\xff\xa0\xc6\x65\xf9\x5f\x38\xf3\x7f\x25\x53\x6b\x6a\xc4\xd3\xf1\xf1\xa9\x8f\xb8\xea\x64\x34\xf1\xd9\x3e\xbe\x6f\xc6\xa3\x4c\xf5\x70\xea\x1c\x1d\x87\xf5\xcf\xb7\x94\x65\x8f\xdf\xbb\x4e\x16\xb1\xae\xf6\xa0\xe2\x1d\x8d\x18\xa9\xd4\x2e\xdf\xb7\x1b\xde\x37\x89\xb2\x34\x4c\xcd\xb1\xdd\x67\x5a\xd0\x04\x6d\x47\xfa\x7a\x74\x95\xbe\x6e\x15\xc7\x27\xaf\x2a\x32\x1b\x88\xe2\x7e\x03\x62\x7f\x49\x79\xb8\x52\x83\x77\x54\x40\xed\xef\xdc\x0c\x4a\x8b\xfd\x45\x0f\xf4\x72\xa2\x34\x3c\xd2\x7e\x4f\x8f\xa5\xeb\xe8\xd0\x9f\x6b\x4f\xa2\x6a\xf7\x65\x8a\x4f\xae\x3a\xe3\x3f\xad\xbf\x51\x2e\x8c\x75\xd4\xbc\x67\xd6\xcb\x03\xf6\x68\xf4\x44\x11\xe1\xf4\x6f\x6a\x94\x90\xf6\xed\x73\x78\x8f\x91\xd5\x5c\xf6\xb4\x12\xeb\x64\x10\x47\xcd\xdb\x29\xf1\x6f\xb3\x7a\x07\xbf\x02\x00\x00\xff\xff\x75\xd7\x68\x3b\x89\x07\x00\x00")
 
 func collectionHtmlBytes() ([]byte, error) {
 	return bindataRead(
@@ -283,7 +283,7 @@
 	return a, nil
 }
 
-var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\x5b\x6b\x23\x37\x14\x7e\xf7\xaf\x38\x98\x90\x26\xdb\x7a\xdc\x5d\x68\x1f\x16\x7b\x4a\x2e\x5b\x30\x6c\xc2\x42\xd2\xbe\x6b\xa4\x63\x8f\x40\x96\x06\x49\x36\x35\xc3\xfc\xf7\x1e\xcd\x68\x6e\xb6\x93\xae\xcb\xfa\x69\xa4\x73\xfd\xbe\xef\x48\x72\x59\xce\x3f\x4c\x80\x7e\x0f\xa6\x38\x58\xb9\xc9\x3d\x7c\xfa\xf5\xe3\xef\xf0\x9a\x23\xfc\xcd\x34\x13\x72\xb7\x85\xbb\x9d\xcf\x8d\x75\x09\xdc\x29\x05\xb5\x93\x03\x8b\x0e\xed\x1e\x45\x52\x47\xff\xe5\x10\xcc\x1a\x7c\x2e\x1d\x38\xb3\xb3\x1c\x81\x1b\x81\x40\xcb\x8d\xd9\xa3\xd5\x28\x20\x3b\x00\x83\xfb\x97\xc7\x99\xf3\x07\x85\x75\x98\x92\x1c\x35\x85\xfa\x9c\x79\xe0\x4c\x43\x86\xb0\x36\x3b\x2d\x40\x6a\xda\x44\xf8\xba\x7a\xf8\xf2\xfc\xf2\x05\xd6\x52\x61\x32\xf9\x30\xaf\xaa\xc9\xa4\x2c\x05\xae\xa5\x46\x98\x72\xa3\x3d\x6a\x3f\x0d\xbb\x0b\x87\xdc\x4b\xa3\x81\x2b\xe6\xdc\x72\x1a\x97\xb3\x19\x55\xf0\x68\x61\x2b\xd4\x6c\x63\xa5\x98\xa6\x75\xe5\x45\xfe\x5b\xfa\x72\xd0\x3c\x63\x0e\x17\x73\x5a\x34\xbb\x42\xee\xdb\x04\x21\x80\x23\x01\x6e\x3f\x66\xb3\x8f\x9f\x66\xdc\xa8\x98\x01\xe0\x85\x08\x20\x00\x30\x2d\xcb\x24\x7e\x27\x7f\xee\x94\x7a\x66\x5b\xac\xaa\x69\x93\x71\x4e\x29\xd3\xc9\x62\x1e\xfb\x49\x43\xff\x96\xe9\x0d\x42\xf2\x6a\x91\xfc\xc8\xed\xc2\xde\xcb\x12\xae\x44\x06\x9f\x97\x90\x3c\x32\xcf\x02\x84\x64\x25\xa0\x4e\xd5\x20\x6b\xb7\x43\x6b\xe4\x9a\xc4\x8e\x7a\xa0\x97\x42\x05\xb8\x2b\x0a\xb8\x57\xe8\x9c\xd4\x9b\x36\x6d\xbb\x6e\xc1\x76\x70\xff\x5f\x89\x85\x50\xfd\xa2\x63\xe9\xc1\x28\xd5\xf0\xe1\x22\xc2\xd6\xdb\xa7\xbd\xad\x16\x61\x25\x3a\xa4\x64\x1c\xf9\x8a\xf4\xc1\x22\xf3\xc6\xfe\xe4\xc2\xb0\xda\x11\x96\x10\x38\xc0\x42\x20\xc4\x71\xf4\x82\x41\x6e\x71\x4d\xda\xc4\xa1\x99\xf3\xae\xf6\x1f\x7a\x49\x7c\xd4\x23\x80\xb6\x69\xe0\x5a\x64\xcb\x63\x8e\xae\x85\x5e\x0e\xe5\xb8\xe6\xc1\x67\x5c\xfb\x9a\xeb\xe5\x08\x48\xfa\x64\x2c\x26\x49\xb2\x98\xb3\x74\xdc\x57\x59\xa2\x72\x78\x42\xc9\x8d\x36\xd0\xb7\xe6\x6e\x29\xc8\x8f\x82\xb4\xe8\x62\xc8\x16\x19\x1f\xe8\x76\x99\x6a\xe7\x35\x0b\x27\x6b\x63\xcd\xae\x38\x95\xac\x33\x7d\x86\x70\x6c\xda\x55\x0f\xf9\x8c\x74\xa1\x48\xd8\x6e\x79\xaa\x31\x05\xc3\x71\x86\x9e\xc8\x9a\xab\x1a\xdf\x59\x35\x63\xc2\x47\x74\xdc\xca\x22\x30\xd5\xe5\xac\x4f\x73\x81\x3c\x19\x18\xdb\xa1\xf8\xcf\x84\xdf\x76\x99\x92\x2e\x87\xf6\x6a\x81\x80\x69\xd4\x6e\xc8\x1c\xbd\x5a\xa7\x88\xfb\xdd\x02\x83\xe5\x11\xe9\x63\xea\xaf\x3c\xdb\xfc\x02\x57\x8c\x73\x62\xe2\xab\x74\xbe\xbe\x26\x9a\xaa\x68\xb7\x0e\x46\x82\xf4\xb2\xd0\x60\x52\x64\x55\x41\x41\x5e\x92\x48\x8c\x8c\x9c\x71\x16\xe9\xcf\xe4\xdd\x57\x48\x56\x3a\x76\x7f\xd6\x79\x36\x76\x7e\x36\xfe\x2d\xff\xf1\x70\xc6\x0c\xf3\x31\xd6\x77\xc9\xa7\x03\x2a\xff\x41\x77\xca\x77\x34\x7c\xef\x58\x3c\xd1\x13\xe4\x5f\x59\xa6\xce\xe4\x1a\xd8\xbe\x37\xdd\xca\x7d\xb3\x72\xcf\xfc\xe9\x20\x74\x96\x1f\x26\xff\x16\xb7\x19\x5a\x9a\x00\xa9\xd7\xa6\xd6\xfe\xa9\xde\x71\x6f\xe8\xde\x58\xdf\x96\x9a\xc4\x6b\x52\xd2\x68\xdc\x84\x81\xa5\x86\x8d\x95\xfe\x10\xae\xb2\x50\x23\x19\x6e\x92\xd3\xbd\x32\xd9\x23\xee\x5f\x0f\x05\x76\x2e\x83\xbd\xaa\xba\xfd\x31\xd2\xbf\x73\xfd\xb9\xee\xf6\xb9\xf4\xf6\x1b\x3c\xd3\x83\xfc\x97\xfe\xb5\xb8\xf0\xd9\xbb\x79\x36\x20\xe2\x83\xed\x6e\x8f\x1f\xd2\xc5\xb0\xa3\xba\xfb\x49\xf7\xf5\x6f\x00\x00\x00\xff\xff\xdd\x03\x4c\x02\xbd\x09\x00\x00")
+var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\xdf\x6f\x22\x37\x10\x7e\xe7\xaf\x18\xa1\x28\x4d\xae\x65\xe9\x9d\xd4\x3e\x9c\x60\xab\x24\x5c\x25\xa4\x4b\x74\x12\x69\xdf\xbd\xf6\x00\x96\x8c\xbd\xb2\x0d\x2a\x5a\xed\xff\xde\xf1\xae\xf7\x17\x90\xf4\xa8\x2e\x4f\x78\xe6\x9b\x1f\xdf\x37\xb3\x76\x8a\x62\xfa\x61\x04\xf4\xf7\x64\xf2\xa3\x95\x9b\xad\x87\x4f\xbf\x7e\xfc\x1d\x5e\xb7\x08\x7f\x33\xcd\x84\xdc\xef\xe0\x61\xef\xb7\xc6\xba\x04\x1e\x94\x82\x0a\xe4\xc0\xa2\x43\x7b\x40\x91\x54\xd1\x7f\x39\x04\xb3\x06\xbf\x95\x0e\x9c\xd9\x5b\x8e\xc0\x8d\x40\xa0\xe3\xc6\x1c\xd0\x6a\x14\x90\x1d\x81\xc1\xe3\x6a\x31\x71\xfe\xa8\xb0\x0a\x53\x92\xa3\xa6\x50\xbf\x65\x1e\x38\xd3\x90\x21\xac\xcd\x5e\x0b\x90\x9a\x8c\x08\x5f\x97\x4f\x5f\x5e\x56\x5f\x60\x2d\x15\x26\xa3\x0f\xd3\xb2\x1c\x8d\x8a\x42\xe0\x5a\x6a\x84\x31\x37\xda\xa3\xf6\xe3\x60\x9d\x39\xe4\x5e\x1a\x0d\x5c\x31\xe7\xe6\xe3\x78\x9c\x4c\xa8\x82\x47\x0b\x3b\xa1\x26\x1b\x2b\xc5\x38\xad\x2a\xcf\xb6\xbf\xa5\xab\xa3\xe6\x19\x73\x38\x9b\xd2\xa1\xb6\x0a\x79\x68\x12\x84\x00\x8e\x44\xb8\xf9\x31\x99\x7c\xfc\x34\xe1\x46\xc5\x0c\x00\x2b\x12\x80\x08\xc0\xb8\x28\x92\x57\x8b\x98\x44\x43\xf2\xe7\x5e\xa9\x17\xb6\xc3\xb2\x1c\xd7\x69\xa7\x94\x37\x1d\xcd\xa6\xb1\xa9\x34\x90\xb0\x4c\x6f\x10\xea\xc0\x45\xe6\x88\x03\x01\xaf\x23\x51\x14\x70\x23\x32\xf8\x3c\x87\x64\xc1\x3c\x0b\x5c\x92\xa5\x80\x2a\x55\x4d\xb1\x31\x87\x1e\x09\x9a\xc4\xae\x3a\xc6\xd7\x72\x06\x78\xc8\x73\x78\x54\xe8\x9c\xd4\x9b\x26\x6d\x73\x6e\x08\xb7\x94\xff\x5f\x89\x99\x50\xdd\xa1\x55\xea\xc9\x28\x55\xeb\xe1\x22\xc3\x06\xed\xd3\xce\x57\x4d\x63\x29\x5a\xa6\xe4\x1c\x60\x45\xfa\x64\x91\x79\x63\x7f\x72\x61\x6b\xed\x80\x4b\x08\xec\x71\x21\x12\xe2\x34\x7a\xc6\x60\x6b\x71\x4d\xb3\x89\xdb\x33\xe5\x6d\xed\x3f\xf4\x9c\xf4\xa8\xd6\x00\x6d\xdd\xc0\xad\xc8\xe6\xa7\x1a\xdd\x0a\x3d\xef\x8f\xe3\x96\x07\xcc\xb0\xf6\x2d\xd7\xf3\x01\x91\xf4\xd9\x58\x4c\x92\x64\x36\x65\xe9\xb0\xaf\xa2\x40\xe5\xf0\x4c\x92\x3b\x6d\xa0\x6b\xcd\xdd\x53\x90\x1f\x04\x69\xd1\xc6\x90\x2f\x2a\xde\x9b\xdb\x75\x53\xbb\x3c\xb3\xf0\x89\x6d\xac\xd9\xe7\xe7\x23\x6b\x5d\x9f\x09\xde\x01\x3b\xca\x17\x46\x17\x8a\x04\x73\xa3\x53\xc5\x29\x38\x4e\x33\x74\x42\x56\x5a\x55\xfc\x2e\x4e\x33\x26\x5c\xa0\xe3\x56\xe6\x41\xa9\x36\x67\xd8\x87\x55\x8e\x3c\xe9\x39\x9b\xa5\xf8\xcf\x84\xdf\xf6\x99\x92\x6e\x0b\xcd\x1d\x03\x81\xd3\xa0\xdd\x90\x39\xa2\x1a\x50\xe4\xfd\x6e\x81\xde\xf1\x44\xf4\xa1\xf4\x37\x9e\x6d\x7e\x81\x1b\xc6\x39\x29\xf1\x55\x3a\x5f\x5d\x13\x75\x55\xb4\x3b\x07\x83\x81\x74\x63\xa1\xc5\xa4\xc8\xb2\x84\x9c\x50\x92\x44\x8c\x8a\x5c\x00\x8b\xf4\x67\x42\x77\x15\x92\xa5\x8e\xdd\x5f\x04\x4f\x86\xe0\x17\xe3\xdf\xc2\x0f\x97\x33\x66\x98\x0e\xb9\xbe\x2b\x3e\x7d\xa0\xf2\x1f\x74\xe7\x7a\x47\xc7\xf7\xae\xc5\x33\xbd\x45\xfe\x95\x65\xea\x42\xae\x9e\xef\x7b\xd3\x2d\xdd\x37\x2b\x0f\xcc\x9f\x2f\x42\xeb\xf9\x61\xe3\xdf\xe1\x2e\x43\x4b\x1b\x20\xf5\xda\x54\xb3\x7f\xae\x2c\xee\x8d\xb9\xd7\xde\xb7\x47\x4d\xc3\xab\x53\xd2\x6a\xdc\x85\x85\xa5\x86\x8d\x95\xfe\x18\xae\xb2\x50\x23\xe9\x1b\x09\xf4\xa8\x4c\xb6\xc0\xc3\xeb\x31\xc7\x16\xd2\xb3\x95\xe5\xfd\x8f\x19\xfd\x3b\xd7\x9f\x6b\x6f\x9f\x6b\x6f\xbf\xde\x53\xdd\xcb\x7f\xed\xff\x18\x57\x3e\x7b\x77\x2f\x06\x44\x7c\xb0\xdd\xfd\xe9\x43\x3a\xeb\x77\x54\x75\x3f\x6a\x7f\xfd\x1b\x00\x00\xff\xff\x14\x60\xd4\x06\xc6\x09\x00\x00")
 
 func syncbaseHtmlBytes() ([]byte, error) {
 	return bindataRead(
diff --git a/services/debug/debug/browseserver/assets/collection.html b/services/debug/debug/browseserver/assets/collection.html
index 6eaad84..9d9f271 100644
--- a/services/debug/debug/browseserver/assets/collection.html
+++ b/services/debug/debug/browseserver/assets/collection.html
@@ -7,33 +7,33 @@
 {{define "content"}}
 
 <section class="section--center mdl-grid">
-    <h5>Collection {{.Database.Id.Name}}/{{.Collection.Id.Name}}</h5>
+    <h5>Collection {{.Tree.Database.Id.Name}}/{{.Tree.Collection.Id.Name}}</h5>
     <div class="mdl-cell mdl-cell--12-col">
         <dl>
-          <dt>App Blessing:<dt><dd>{{.Database.Id.Blessing}}</dd>
-          <dt>Creator's User Blessing:<dt><dd>{{.Collection.Id.Blessing}}</dd>
-          <dt>Number of Rows:<dt><dd>{{.RowCount}}</dd>
-          <dt>Total Key Size:<dt><dd>{{.TotKeySize}}</dd>
+          <dt>App Blessing:<dt><dd>{{.Tree.Database.Id.Blessing}}</dd>
+          <dt>Creator's User Blessing:<dt><dd>{{.Tree.Collection.Id.Blessing}}</dd>
+          <dt>Number of Rows:<dt><dd>{{.Tree.RowCount}}</dd>
+          <dt>Total Key Size:<dt><dd>{{.Tree.TotKeySize}}</dd>
           <dt>Keys<dt>
           <dd>
             <table>
               <tr><th></th><th>Key</th><th>Value</th></tr>
               <tr><th colspan="3">
-                {{if .KeysPage.HasPrev}}
-                  <a href="collection?n={{$.ServerName}}&db={{.Database.Id.Blessing}}&dn={{.Database.Id.Name}}&cb={{.Collection.Id.Blessing}}&cn={{.Collection.Id.Name}}">
+                {{if .Tree.KeysPage.HasPrev}}
+                  <a href="collection?n={{$.ServerName}}&db={{.Tree.Database.Id.Blessing}}&dn={{.Tree.Database.Id.Name}}&cb={{.Tree.Collection.Id.Blessing}}&cn={{.Tree.Collection.Id.Name}}">
                     [back]
                   </a>
                 {{end}}
                 &nbsp;
               </th></tr>
-              {{range .KeysPage.KeyVals}}
+              {{range .Tree.KeysPage.KeyVals}}
                 <tr><td>{{.Index}}</td><td>{{.Key}}</td><td><code>{{.Value}}</code></td></tr>
               {{else}}
                 <tr><td colspan="3">(no keys)</td></tr>
               {{end}}
               <tr><th colspan="3">
-                {{if len .KeysPage.NextKey}}
-                  <a href="collection?n={{$.ServerName}}&db={{.Database.Id.Blessing}}&dn={{.Database.Id.Name}}&cb={{.Collection.Id.Blessing}}&cn={{.Collection.Id.Name}}&firstkey={{.KeysPage.NextKey}}">
+                {{if len .Tree.KeysPage.NextKey}}
+                  <a href="collection?n={{$.ServerName}}&db={{.Tree.Database.Id.Blessing}}&dn={{.Tree.Database.Id.Name}}&cb={{.Tree.Collection.Id.Blessing}}&cn={{.Tree.Collection.Id.Name}}&firstkey={{.Tree.KeysPage.NextKey}}">
                     (next)
                   </a>
                 {{end}}
diff --git a/services/debug/debug/browseserver/assets/syncbase.html b/services/debug/debug/browseserver/assets/syncbase.html
index 5b87667..18d1243 100644
--- a/services/debug/debug/browseserver/assets/syncbase.html
+++ b/services/debug/debug/browseserver/assets/syncbase.html
@@ -9,11 +9,11 @@
 <section class="section--center mdl-grid">
     <h5>Syncbase</h5>
     <div class="mdl-cell mdl-cell--12-col">
-      Service "{{.Service.FullName}}"
+      Service "{{.Tree.Service.FullName}}"
     </div>
 </section>
 
-{{range .Tree}}
+{{range .Tree.Dbs}}
   <section class="section--center mdl-grid">
     {{ $db := .Database.Id }}
     <h5>Database "{{$db.Name}}"</h5>
diff --git a/services/debug/debug/browseserver/browseserver.go b/services/debug/debug/browseserver/browseserver.go
index c0e0140..f4b4797 100644
--- a/services/debug/debug/browseserver/browseserver.go
+++ b/services/debug/debug/browseserver/browseserver.go
@@ -25,17 +25,15 @@
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/naming"
-	"v.io/v23/rpc/reserved"
 	"v.io/v23/security"
 	"v.io/v23/services/logreader"
 	"v.io/v23/services/stats"
-	sbwire "v.io/v23/services/syncbase"
 	svtrace "v.io/v23/services/vtrace"
-	"v.io/v23/syncbase"
 	"v.io/v23/uniqueid"
 	"v.io/v23/verror"
 	"v.io/v23/vom"
 	"v.io/v23/vtrace"
+	"v.io/x/ref/services/debug/debug/browseserver/sbtree"
 	"v.io/x/ref/services/internal/pproflib"
 )
 
@@ -825,23 +823,6 @@
 // collections, and has links to the detailed collection page.
 type syncbaseHandler struct{ *handler }
 
-func implementsSyncbaseInterface(ctx *context.T, server string) (bool, error) {
-	const (
-		syncbasePkgPath = "v.io/v23/services/syncbase"
-		syncbaseName    = "Service"
-	)
-	interfaces, err := reserved.Signature(ctx, server)
-	if err != nil {
-		return false, err
-	}
-	for _, ifc := range interfaces {
-		if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
-			return true, nil
-		}
-	}
-	return false, nil
-}
-
 func internalServerError(w http.ResponseWriter, doing string, err error) {
 	w.WriteHeader(http.StatusInternalServerError)
 	fmt.Fprintf(w, "Problem %s: %v", doing, err)
@@ -862,99 +843,38 @@
 		return
 	}
 
-	hasSyncbase, err := implementsSyncbaseInterface(ctx, server)
+	sbTree, err := sbtree.AssembleSyncbaseTree(ctx, server)
+
 	if err != nil {
-		internalServerError(w, "getting interfaces", err)
-		return
-	}
-
-	if !hasSyncbase {
-		args := struct {
-			ServerName  string
-			CommandLine string
-			Vtrace      *Tracer
-		}{
-			ServerName:  server,
-			CommandLine: "(no command line)",
-			Vtrace:      tracer,
-		}
-		h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
-		return
-	}
-
-	service := syncbase.NewService(server)
-
-	dbIds, err := service.ListDatabases(ctx)
-	if err != nil {
-		internalServerError(w, "listing databases", err)
-		return
-	}
-
-	// Assemble the data to be displayed as tree of nested slices
-	type SyncgroupTree struct {
-		Syncgroup syncbase.Syncgroup
-		Spec      sbwire.SyncgroupSpec
-		Members   map[string]sbwire.SyncgroupMemberInfo
-	}
-	type databaseTree struct {
-		Database    syncbase.Database
-		Collections []syncbase.Collection
-		Syncgroups  []SyncgroupTree
-	}
-	tree := make([]databaseTree, len(dbIds))
-	for i := range dbIds {
-		// TODO(eobrain) Confirm nil for schema is appropriate
-		db := service.DatabaseForId(dbIds[i], nil)
-
-		// Assemble collections
-		collIds, err := db.ListCollections(ctx)
-		if err != nil {
-			internalServerError(w, "listing collections", err)
-			return
-		}
-		colls := make([]syncbase.Collection, len(collIds))
-		for j := range collIds {
-			colls[j] = db.CollectionForId(collIds[i])
-		}
-
-		// Assemble syncgroups
-		sgIds, err := db.ListSyncgroups(ctx)
-		if err != nil {
-			internalServerError(w, "listing syncgroups", err)
-			return
-		}
-		sgs := make([]SyncgroupTree, len(sgIds))
-		for j := range sgIds {
-			sg := db.SyncgroupForId(sgIds[j])
-			spec, _, err := sg.GetSpec(ctx)
-			if err != nil {
-				internalServerError(w, "getting spec of syncgroup", err)
-				return
+		if err == sbtree.NoSyncbaseError {
+			// Error because no Syncbase, send to no-syncbase page.
+			args := struct {
+				ServerName  string
+				CommandLine string
+				Vtrace      *Tracer
+			}{
+				ServerName:  server,
+				CommandLine: "(no command line)",
+				Vtrace:      tracer,
 			}
-			members, err := sg.GetMembers(ctx)
-			if err != nil {
-				internalServerError(w, "getting members of syncgroup", err)
-				return
-			}
-			sgs[j] = SyncgroupTree{sg, spec, members}
+			h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
+		} else {
+			// Some other error.
+			internalServerError(w, "getting syncbase information", err)
 		}
-
-		tree[i] = databaseTree{db, colls, sgs}
+		return
 	}
 
-	// Assemble data and send it to the template to generate HTML
 	args := struct {
 		ServerName  string
 		CommandLine string
 		Vtrace      *Tracer
-		Service     syncbase.Service
-		Tree        []databaseTree
+		Tree        *sbtree.SyncbaseTree
 	}{
 		ServerName:  server,
 		CommandLine: fmt.Sprintf(`debug glob "%s/*"`, server),
 		Vtrace:      tracer,
-		Service:     service,
-		Tree:        tree,
+		Tree:        sbTree,
 	}
 	h.execute(h.ctx, w, r, syncbaseTmpl, args)
 }
@@ -963,76 +883,7 @@
 // main Syncbase viewer page.
 type collectionHandler struct{ *handler }
 
-type keyVal struct {
-	Index int
-	Key   string
-	Value interface{}
-}
-
-// keysPage is a contiguous subset of the keys, used for pagination.
-type keysPage struct {
-	HasPrev bool
-	KeyVals []keyVal
-	NextKey string
-}
-
-// scanCollection gets some statistics, plus a page of key-value pairs starting
-// with firstKey.
-func scanCollection(
-	ctx *context.T, coll syncbase.Collection, firstKey string,
-) (rowCount int, totKeySize uint64, page keysPage) {
-	const keysPerPage = 7
-	page.KeyVals = make([]keyVal, 0, keysPerPage)
-
-	stream := coll.Scan(ctx, syncbase.Prefix(""))
-
-	// We scan through all the keys lexicographically, and when we come to a
-	// key >= firstKey we start gathering a "page" of keys. As we scan, a
-	// state machine keeps track of whether we are before the page, in the
-	// page, or after the page.
-	const (
-		before    = 0
-		gathering = iota
-		done      = iota
-	)
-	state := before
-	for stream.Advance() {
-		key := stream.Key()
-		totKeySize += uint64(len(key))
-
-		switch state {
-		case before:
-			if key >= firstKey {
-				// First key found: transition to gathering the page
-				state = gathering
-			} else {
-				// There is at least one key before the page
-				page.HasPrev = true
-			}
-		case gathering:
-			if len(page.KeyVals) >= keysPerPage {
-				// Page full: transition to done.  There is at
-				// least one key after the page.
-				state = done
-				page.NextKey = key
-			}
-		case done:
-			// Done gathering.  Terminal state.
-		}
-		if state == gathering {
-			// Grab the value, put it and the key into a KeyVal, and
-			// add it to the page.
-			var value interface{}
-			err := stream.Value(&value)
-			if err != nil {
-				value = fmt.Sprintf("ERROR getting value: %v", err)
-			}
-			page.KeyVals = append(page.KeyVals, keyVal{rowCount, key, value})
-		}
-		rowCount++
-	}
-	return
-}
+const keysPerPage = 7
 
 func (h *collectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	var (
@@ -1042,8 +893,6 @@
 		collBlessing = r.FormValue("cb")
 		collName     = r.FormValue("cn")
 		firstKey     = r.FormValue("firstkey")
-		dbId         = sbwire.Id{dbBlessing, dbName}
-		collId       = sbwire.Id{collBlessing, collName}
 	)
 	ctx, tracer := newTracer(h.ctx)
 	if len(server) == 0 {
@@ -1051,35 +900,23 @@
 		return
 	}
 
-	service := syncbase.NewService(server)
-
-	// TODO(eobrain) Confirm nil for schema is appropriate
-	db := service.DatabaseForId(dbId, nil)
-	coll := db.CollectionForId(collId)
-
-	rowCount, totKeySize, page := scanCollection(ctx, coll, firstKey)
+	collTree := sbtree.AssembleCollectionTree(
+		ctx, server,
+		dbBlessing, dbName,
+		collBlessing, collName,
+		firstKey, keysPerPage)
 
 	// Assemble data and send it to the template to generate HTML
 	args := struct {
 		ServerName  string
 		CommandLine string
 		Vtrace      *Tracer
-		Service     syncbase.Service
-		Database    syncbase.Database
-		Collection  syncbase.Collection
-		RowCount    int
-		TotKeySize  uint64
-		KeysPage    keysPage
+		Tree        *sbtree.CollectionTree
 	}{
 		ServerName:  server,
 		CommandLine: "(no command line)",
 		Vtrace:      tracer,
-		Service:     service,
-		Database:    db,
-		Collection:  coll,
-		RowCount:    rowCount,
-		TotKeySize:  totKeySize,
-		KeysPage:    page,
+		Tree:        collTree,
 	}
 	h.execute(h.ctx, w, r, collectionTmpl, args)
 }
diff --git a/services/debug/debug/browseserver/sbtree/colltree.go b/services/debug/debug/browseserver/sbtree/colltree.go
new file mode 100644
index 0000000..5fd2c37
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/colltree.go
@@ -0,0 +1,113 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree
+
+import (
+	"fmt"
+
+	"v.io/v23/context"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/syncbase"
+)
+
+// CollectionTree has all the data for the collection page of the Syncbase debug
+// viewer.
+type CollectionTree struct {
+	Service    syncbase.Service
+	Database   syncbase.Database
+	Collection syncbase.Collection
+	RowCount   int
+	TotKeySize uint64
+	KeysPage   keysPage
+}
+
+type keyVal struct {
+	Index int
+	Key   string
+	Value interface{}
+}
+
+// keysPage is a contiguous subset of the keys, used for pagination.
+type keysPage struct {
+	HasPrev bool
+	KeyVals []keyVal
+	NextKey string
+}
+
+// AssembleCollectionTree returns information describing the given Syncbase
+// collection, including a "page" of keys starting at the given first key.
+func AssembleCollectionTree(
+	ctx *context.T, server, dbBlessing, dbName, collBlessing, collName, firstKey string, keysPerPage int,
+) *CollectionTree {
+	var (
+		dbId    = wire.Id{dbBlessing, dbName}
+		collId  = wire.Id{collBlessing, collName}
+		service = syncbase.NewService(server)
+
+		// TODO(eobrain) Confirm nil for schema is appropriate
+		db   = service.DatabaseForId(dbId, nil)
+		coll = db.CollectionForId(collId)
+	)
+
+	rowCount, totKeySize, page := scanCollection(ctx, coll, firstKey, keysPerPage)
+	return &CollectionTree{service, db, coll, rowCount, totKeySize, page}
+}
+
+// scanCollection gets some statistics, plus a page of key-value pairs starting
+// with firstKey.
+func scanCollection(
+	ctx *context.T, coll syncbase.Collection, firstKey string, keysPerPage int,
+) (rowCount int, totKeySize uint64, page keysPage) {
+	page.KeyVals = make([]keyVal, 0, keysPerPage)
+
+	stream := coll.Scan(ctx, syncbase.Prefix(""))
+
+	// We scan through all the keys lexicographically, and when we come to a
+	// key >= firstKey we start gathering a "page" of keys. As we scan, a
+	// state machine keeps track of whether we are before the page, in the
+	// page, or after the page.
+	const (
+		before    = 0
+		gathering = iota
+		done      = iota
+	)
+	state := before
+	for stream.Advance() {
+		key := stream.Key()
+		totKeySize += uint64(len(key))
+
+		switch state {
+		case before:
+			if key >= firstKey {
+				// First key found: transition to gathering the page
+				state = gathering
+			} else {
+				// There is at least one key before the page
+				page.HasPrev = true
+			}
+		case gathering:
+			if len(page.KeyVals) >= keysPerPage {
+				// Page full: transition to done.  There is at
+				// least one key after the page.
+				state = done
+				page.NextKey = key
+			}
+		case done:
+			// Done gathering.  Terminal state.
+		}
+		if state == gathering {
+			// Grab the value, put it and the key into a KeyVal, and
+			// add it to the page.
+			var value interface{}
+			err := stream.Value(&value)
+			if err != nil {
+				value = fmt.Sprintf("ERROR getting value: %v", err)
+			}
+			page.KeyVals = append(page.KeyVals, keyVal{rowCount, key, value})
+		}
+		rowCount++
+	}
+	return
+}
diff --git a/services/debug/debug/browseserver/sbtree/colltree_test.go b/services/debug/debug/browseserver/sbtree/colltree_test.go
new file mode 100644
index 0000000..921e0db
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/colltree_test.go
@@ -0,0 +1,351 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree_test
+
+import (
+	"reflect"
+	"testing"
+
+	"v.io/v23/syncbase"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/debug/debug/browseserver/sbtree"
+	tu "v.io/x/ref/services/syncbase/testutil"
+)
+
+func TestEmptyCollection(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		db      = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll    = tu.CreateCollection(t, ctx, db, "the_collection")
+	)
+
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"",
+		100,
+	)
+
+	if got.Service.FullName() != serverName {
+		t.Errorf("got %q, want %q", got.Service.FullName(), serverName)
+	}
+	if got.Database.Id().Name != "the_db" {
+		t.Errorf(`got %q, want "the_db"`, got.Database.Id().Name)
+	}
+	if got.Collection.Id().Name != "the_collection" {
+		t.Errorf(`got %q, want "the_collection"`, got.Database.Id().Name)
+	}
+	if got.RowCount != 0 {
+		t.Errorf("got %d rows, want none", got.RowCount)
+	}
+	if got.TotKeySize != 0 {
+		t.Errorf("got %d bytes of keys, want zero", got.TotKeySize)
+	}
+	if got.KeysPage.HasPrev {
+		t.Error("Got previous page, want no previous page")
+	}
+	if len(got.KeysPage.KeyVals) != 0 {
+		t.Errorf("Want no keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	if got.KeysPage.NextKey != "" {
+		t.Errorf("Got %q, want empty string", got.KeysPage.NextKey)
+	}
+}
+
+func TestSingleKeysPage(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service        = syncbase.NewService(serverName)
+		db             = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll           = tu.CreateCollection(t, ctx, db, "the_collection")
+		wantTotKeySize = len("Bravo") + len("Alfa") + len("Delta") + len("Charlie")
+	)
+	// Put keys of sundry types in non-alphabethical order
+	coll.Put(ctx, "Bravo", int64(9999))
+	coll.Put(ctx, "Alfa", complex(11, 22))
+	coll.Put(ctx, "Delta", "something")
+	coll.Put(ctx, "Charlie", 'x')
+
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"",
+		100,
+	)
+
+	if got.RowCount != 4 {
+		t.Errorf("got %d rows, want 4", got.RowCount)
+	}
+	if int(got.TotKeySize) != wantTotKeySize {
+		t.Errorf("got %d bytes of keys, want %d", got.TotKeySize, wantTotKeySize)
+	}
+	if got.KeysPage.HasPrev {
+		t.Error("Got previous page, want no previous page")
+	}
+	if len(got.KeysPage.KeyVals) != 4 {
+		t.Errorf("Wanted 4 keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	for i := 0; i < 4; i++ {
+		if got.KeysPage.KeyVals[i].Index != i {
+			t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i)
+		}
+	}
+	// Make sure keys come back in alphabetical order
+	for i, want := range []string{"Alfa", "Bravo", "Charlie", "Delta"} {
+		if got.KeysPage.KeyVals[i].Key != want {
+			t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+		}
+	}
+	for i, want := range []interface{}{complex(11, 22), int64(9999), 'x', "something"} {
+		if got.KeysPage.KeyVals[i].Value != want {
+			t.Errorf("got %v of type %T, want %v of type %T",
+				got.KeysPage.KeyVals[i].Value, got.KeysPage.KeyVals[i].Value, want, want)
+		}
+	}
+
+	if got.KeysPage.NextKey != "" {
+		t.Errorf("Got %q, want empty string", got.KeysPage.NextKey)
+	}
+}
+
+func TestFirstOfMultiplePages(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		db      = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll    = tu.CreateCollection(t, ctx, db, "the_collection")
+	)
+	// Ten keys, in pages of four, starting with the first key
+	coll.Put(ctx, "555", 99)
+	coll.Put(ctx, "000", 99)
+	coll.Put(ctx, "999", 99)
+	coll.Put(ctx, "111", 99)
+	coll.Put(ctx, "444", 99)
+	coll.Put(ctx, "666", 99)
+	coll.Put(ctx, "222", 99)
+	coll.Put(ctx, "777", 99)
+	coll.Put(ctx, "333", 99)
+	coll.Put(ctx, "888", 99)
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"",
+		4,
+	)
+
+	if got.RowCount != 10 {
+		t.Errorf("got %d rows, want 10", got.RowCount)
+	}
+	if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+		t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+	}
+	if got.KeysPage.HasPrev {
+		t.Error("Got previous page, want no previous page")
+	}
+	if len(got.KeysPage.KeyVals) != 4 {
+		t.Errorf("Wanted 4 keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	for i := 0; i < 4; i++ {
+		if got.KeysPage.KeyVals[i].Index != i {
+			t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i)
+		}
+	}
+	for i, want := range []string{"000", "111", "222", "333"} {
+		if got.KeysPage.KeyVals[i].Key != want {
+			t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+		}
+	}
+	if got.KeysPage.NextKey != "444" {
+		t.Errorf(`Got %q, want "444"`, got.KeysPage.NextKey)
+	}
+}
+
+func TestMiddleOfMultiplePages(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		db      = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll    = tu.CreateCollection(t, ctx, db, "the_collection")
+	)
+	// Ten keys, in pages of four, starting with fifth key
+	coll.Put(ctx, "555", 99)
+	coll.Put(ctx, "000", 99)
+	coll.Put(ctx, "999", 99)
+	coll.Put(ctx, "111", 99)
+	coll.Put(ctx, "444", 99)
+	coll.Put(ctx, "666", 99)
+	coll.Put(ctx, "222", 99)
+	coll.Put(ctx, "777", 99)
+	coll.Put(ctx, "333", 99)
+	coll.Put(ctx, "888", 99)
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"444",
+		4,
+	)
+
+	if got.RowCount != 10 {
+		t.Errorf("got %d rows, want 10", got.RowCount)
+	}
+	if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+		t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+	}
+	if !got.KeysPage.HasPrev {
+		t.Error("No previous page, want previous page")
+	}
+	if len(got.KeysPage.KeyVals) != 4 {
+		t.Errorf("Wanted 4 keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	for i := 0; i < 4; i++ {
+		if got.KeysPage.KeyVals[i].Index != i+4 {
+			t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i+4)
+		}
+	}
+	for i, want := range []string{"444", "555", "666", "777"} {
+		if got.KeysPage.KeyVals[i].Key != want {
+			t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+		}
+	}
+	if got.KeysPage.NextKey != "888" {
+		t.Errorf(`Got %q, want "888"`, got.KeysPage.NextKey)
+	}
+}
+
+func TestLastOfMultiplePages(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		db      = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll    = tu.CreateCollection(t, ctx, db, "the_collection")
+	)
+	// Ten keys, in pages of four, starting with ninth key.
+	coll.Put(ctx, "555", 99)
+	coll.Put(ctx, "000", 99)
+	coll.Put(ctx, "999", 99)
+	coll.Put(ctx, "111", 99)
+	coll.Put(ctx, "444", 99)
+	coll.Put(ctx, "666", 99)
+	coll.Put(ctx, "222", 99)
+	coll.Put(ctx, "777", 99)
+	coll.Put(ctx, "333", 99)
+	coll.Put(ctx, "888", 99)
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"888",
+		4,
+	)
+
+	if got.RowCount != 10 {
+		t.Errorf("got %d rows, want 10", got.RowCount)
+	}
+	if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+		t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+	}
+	if !got.KeysPage.HasPrev {
+		t.Error("No previous page, want previous page")
+	}
+	if len(got.KeysPage.KeyVals) != 2 {
+		t.Errorf("Wanted 2 keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	for i := 0; i < 2; i++ {
+		if got.KeysPage.KeyVals[i].Index != i+8 {
+			t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i+8)
+		}
+	}
+	// Make sure keys come back in alphabetical order.
+	for i, want := range []string{"888", "999"} {
+		if got.KeysPage.KeyVals[i].Key != want {
+			t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+		}
+	}
+	if got.KeysPage.NextKey != "" {
+		t.Errorf(`Got %q, want empty string`, got.KeysPage.NextKey)
+	}
+}
+
+func TestNonBuiltInType(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		db      = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll    = tu.CreateCollection(t, ctx, db, "the_collection")
+	)
+	type childType struct {
+		I int64
+		C complex128
+	}
+	type someCustomType struct {
+		S string
+		R rune
+		C childType
+	}
+	err := coll.Put(ctx, "the key",
+		someCustomType{"something", 'x', childType{9999, complex(11, 22)}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	got := sbtree.AssembleCollectionTree(
+		ctx,
+		serverName,
+		db.Id().Blessing,
+		"the_db",
+		coll.Id().Blessing,
+		"the_collection",
+		"",
+		100,
+	)
+
+	if got.RowCount != 1 {
+		t.Fatalf("got %d rows, want 1", got.RowCount)
+	}
+	if len(got.KeysPage.KeyVals) != 1 {
+		t.Errorf("Wanted 1 keys, got %v (length %d)",
+			got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+	}
+	value, ok := got.KeysPage.KeyVals[0].Value.(someCustomType)
+	if !ok {
+		t.Fatalf("Got %v of type %T, want of type someCustomType",
+			got.KeysPage.KeyVals[0].Value, got.KeysPage.KeyVals[0].Value)
+	}
+	want := someCustomType{"something", 'x', childType{9999, complex(11, 22)}}
+	if !reflect.DeepEqual(value, want) {
+		t.Errorf("Got %v, want %v", value, want)
+	}
+}
diff --git a/services/debug/debug/browseserver/sbtree/sbtree.go b/services/debug/debug/browseserver/sbtree/sbtree.go
new file mode 100644
index 0000000..e52a34b
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/sbtree.go
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package syncbase provides data structs used by HTML templates to build the
+// web pages for the Syncbase debug viewer.  To minimize mixing of code and
+// presentation, all the data for use in the templates is in a form that is
+// convenient for accessing and iterating over, i.e struct fields, no-arg
+// methods, slices, or maps.  In some cases this required mirroring data
+// structures in the public Syncbase API to avoid having the templates deal with
+// context objects, or to avoid the templates needing extra variables to handle
+// indirection.
+package sbtree
+
+import (
+	"errors"
+	"fmt"
+
+	"v.io/v23/context"
+	"v.io/v23/rpc/reserved"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/syncbase"
+)
+
+// SyncbaseTree has all the data for the main page of the Syncbase debug viewer.
+type SyncbaseTree struct {
+	Service syncbase.Service
+	Dbs     []dbTree
+}
+
+type dbTree struct {
+	Database    syncbase.Database
+	Collections []syncbase.Collection
+	Syncgroups  []syncgroupTree
+}
+
+type syncgroupTree struct {
+	Syncgroup syncbase.Syncgroup
+	Spec      wire.SyncgroupSpec
+	Members   map[string]wire.SyncgroupMemberInfo
+}
+
+// NoSyncbaseError returned as error from AssembleSbTree when the given server
+// does not implement the Syncbase RPC interface.
+var NoSyncbaseError = errors.New("Server does not have Syncbase")
+
+// AssembleSbTree returns information describing the Syncbase server running on
+// the given server. One possible error it can return is NoSyncbaseError,
+// indicating that the server does not implement the Syncbase RPC interface.
+func AssembleSyncbaseTree(ctx *context.T, server string) (*SyncbaseTree, error) {
+	hasSyncbase, err := hasSyncbaseService(ctx, server)
+	if err != nil {
+		return nil, fmt.Errorf("Problem getting interfaces: %v", err)
+	}
+	if !hasSyncbase {
+		return nil, NoSyncbaseError
+	}
+
+	service := syncbase.NewService(server)
+
+	dbIds, err := service.ListDatabases(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("Problem listing databases: %v", err)
+	}
+
+	dbTrees := make([]dbTree, len(dbIds))
+	for i := range dbIds {
+		// TODO(eobrain) Confirm nil for schema is appropriate
+		db := service.DatabaseForId(dbIds[i], nil)
+
+		// Assemble collections
+		collIds, err := db.ListCollections(ctx)
+		if err != nil {
+			return nil, fmt.Errorf("Problem listing collections: %v", err)
+		}
+		colls := make([]syncbase.Collection, len(collIds))
+		for j := range collIds {
+			colls[j] = db.CollectionForId(collIds[j])
+		}
+
+		// Assemble syncgroups
+		sgIds, err := db.ListSyncgroups(ctx)
+		if err != nil {
+			return nil, fmt.Errorf("Problem listing syncgroups: %v", err)
+		}
+		sgs := make([]syncgroupTree, len(sgIds))
+		for j := range sgIds {
+			sg := db.SyncgroupForId(sgIds[j])
+			spec, _, err := sg.GetSpec(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("Problem getting spec of syncgroup: %v", err)
+			}
+			members, err := sg.GetMembers(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("Problem getting members of syncgroup: %v", err)
+			}
+			sgs[j] = syncgroupTree{sg, spec, members}
+		}
+
+		dbTrees[i] = dbTree{db, colls, sgs}
+	}
+
+	return &SyncbaseTree{service, dbTrees}, nil
+}
+
+// hasSyncbaseService determines whether the given server implements the
+// Syncbase interface.
+func hasSyncbaseService(ctx *context.T, server string) (bool, error) {
+	const (
+		syncbasePkgPath = "v.io/v23/services/syncbase"
+		syncbaseName    = "Service"
+	)
+	interfaces, err := reserved.Signature(ctx, server)
+	if err != nil {
+		return false, err
+	}
+	for _, ifc := range interfaces {
+		if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
+			return true, nil
+		}
+	}
+	return false, nil
+}
diff --git a/services/debug/debug/browseserver/sbtree/sbtree_test.go b/services/debug/debug/browseserver/sbtree/sbtree_test.go
new file mode 100644
index 0000000..0c0f4f3
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/sbtree_test.go
@@ -0,0 +1,148 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree_test
+
+import (
+	"testing"
+
+	"v.io/v23/syncbase"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/debug/debug/browseserver/sbtree"
+	tu "v.io/x/ref/services/syncbase/testutil"
+	"v.io/x/ref/test"
+)
+
+func TestWithNoServer(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test, because has long timeout.")
+	}
+	ctx, cleanup := test.V23Init()
+	defer cleanup()
+
+	_, err := sbtree.AssembleSyncbaseTree(ctx, "no-such-server")
+
+	if err == nil || err == sbtree.NoSyncbaseError {
+		t.Errorf("Got %v, want not nil and not %v", err, sbtree.NoSyncbaseError)
+	}
+}
+
+func TestWithEmptyService(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+
+	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got.Service.FullName() != serverName {
+		t.Errorf("got %q, want %q", got.Service.FullName(), serverName)
+	}
+	if len(got.Dbs) != 0 {
+		t.Errorf("want no databases, got %v", got.Dbs)
+	}
+}
+
+func TestWithMultipleEmptyDbs(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service = syncbase.NewService(serverName)
+		dbNames = []string{"db_a", "db_b", "db_c"}
+	)
+	for _, dbName := range dbNames {
+		tu.CreateDatabase(t, ctx, service, dbName)
+	}
+
+	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(got.Dbs) != 3 {
+		t.Fatalf("want 3 databases, got %v", got.Dbs)
+	}
+	for i, db := range got.Dbs {
+		if db.Database.Id().Name != dbNames[i] {
+			t.Errorf("got %q, want %q", db.Database.Id().Name, dbNames[i])
+		}
+		if len(db.Collections) != 0 {
+			t.Errorf("want no collections, got %v", db.Collections)
+		}
+		if len(db.Syncgroups) != 0 {
+			t.Errorf("want no syncgroups, got %v", db.Syncgroups)
+		}
+	}
+}
+
+func TestWithMultipleCollections(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service   = syncbase.NewService(serverName)
+		collNames = []string{"coll_a", "coll_b", "coll_c"}
+		database  = tu.CreateDatabase(t, ctx, service, "the_db")
+	)
+	for _, collName := range collNames {
+		tu.CreateCollection(t, ctx, database, collName)
+	}
+
+	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	for i, coll := range got.Dbs[0].Collections {
+		if coll.Id().Name != collNames[i] {
+			t.Errorf("got %q, want %q", coll.Id().Name, collNames[i])
+		}
+		exists, err := coll.Exists(ctx)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !exists {
+			t.Error("want collection to exist, but it does not")
+		}
+	}
+}
+
+func TestWithMultipleSyncgroups(t *testing.T) {
+	ctx, serverName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	var (
+		service        = syncbase.NewService(serverName)
+		sgNames        = []string{"syncgroup_a", "syncgroup_b", "syncgroup_c"}
+		sgDescriptions = []string{"AAA", "BBB", "CCC"}
+		database       = tu.CreateDatabase(t, ctx, service, "the_db")
+		coll           = tu.CreateCollection(t, ctx, database, "the_collection")
+	)
+	for i, sgName := range sgNames {
+		tu.CreateSyncgroup(t, ctx, database, coll, sgName, sgDescriptions[i])
+	}
+
+	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	for i, sg := range got.Dbs[0].Syncgroups {
+		if sg.Syncgroup.Id().Name != sgNames[i] {
+			t.Errorf("got %q, want %q", sg.Syncgroup.Id().Name, sgNames[i])
+		}
+		if sg.Spec.Description != sgDescriptions[i] {
+			t.Errorf("got %q, want %q", sg.Spec.Description, sgDescriptions[i])
+		}
+		if sg.Spec.Collections[0].Name != "the_collection" {
+			t.Errorf(`got %q, want "the_collection"`,
+				sg.Spec.Collections[0].Name)
+		}
+	}
+}
diff --git a/services/syncbase/testutil/util.go b/services/syncbase/testutil/util.go
index b35e639..e941d22 100644
--- a/services/syncbase/testutil/util.go
+++ b/services/syncbase/testutil/util.go
@@ -67,6 +67,25 @@
 	return c
 }
 
+func CreateSyncgroup(
+	t testing.TB,
+	ctx *context.T,
+	d syncbase.Database,
+	c syncbase.Collection,
+	name, description string,
+) syncbase.Syncgroup {
+	sg := d.SyncgroupForId(CxId(name))
+	sgSpec := wire.SyncgroupSpec{
+		Description: description,
+		Collections: []wire.Id{c.Id()},
+	}
+	sgMembership := wire.SyncgroupMemberInfo{}
+	if err := sg.Create(ctx, sgSpec, sgMembership); err != nil {
+		Fatalf(t, "sg.Create() failed: %v", err)
+	}
+	return sg
+}
+
 // TODO(sadovsky): Drop the 'perms' argument. The only client that passes
 // non-nil, syncgroup_test.go, should use SetupOrDieCustom instead.
 func SetupOrDie(perms access.Permissions) (clientCtx *context.T, serverName string, cleanup func()) {