Bug 378261: Replacing GC_MARK_DEBUG by DumpHeap. r=brendan
authorigor@mir2.org
Wed, 25 Apr 2007 06:43:18 -0700
changeset 786 62464339234195ac33e1d6c6dddaff30594fa533
parent 785 87e5935193518adc8b87461df681b143956703f0
child 787 87ca244eab94192f74409601e70a72afcf9cfef7
push idunknown
push userunknown
push dateunknown
reviewersbrendan
bugs378261
milestone1.9a4pre
Bug 378261: Replacing GC_MARK_DEBUG by DumpHeap. r=brendan
js/jsd/idl/jsdIDebuggerService.idl
js/jsd/jsd_xpc.cpp
js/src/js.c
js/src/jsapi.c
js/src/jsapi.h
js/src/jsatom.c
js/src/jsgc.c
js/src/jsgc.h
js/src/jspubtd.h
js/src/xpconnect/shell/xpcshell.cpp
--- a/js/jsd/idl/jsdIDebuggerService.idl
+++ b/js/jsd/idl/jsdIDebuggerService.idl
@@ -267,16 +267,23 @@ interface jsdIDebuggerService : nsISuppo
     unsigned long unPause();
     
     /**
      * Force the engine to perform garbage collection.
      */
     void GC();
     
     /**
+     * Output dump of JS heap.
+     *
+     * @param fileName Filename to dump the heap into.
+     */
+    void DumpHeap(in string fileName);
+
+    /**
      * Clear profile data for all scripts.
      */
     void clearProfileData();
     
     /**
      * Adds an execution hook filter.  These filters are consulted each time one
      * of the jsdIExecutionHooks is about to be called.  Filters are matched in
      * a first in, first compared fashion.  The first filter to match determines
--- a/js/jsd/jsd_xpc.cpp
+++ b/js/jsd/jsd_xpc.cpp
@@ -2696,41 +2696,51 @@ jsdService::EnumerateScripts (jsdIScript
         if (NS_FAILED(rv))
             break;
     }
     JSD_UnlockScriptSubsystem(mCx);
 
     return rv;
 }
 
-#ifdef GC_MARK_DEBUG
-JS_BEGIN_EXTERN_C
-JS_FRIEND_DATA(FILE *) js_DumpGCHeap;
-JS_END_EXTERN_C
-#endif
-
 NS_IMETHODIMP
 jsdService::GC (void)
 {
     ASSERT_VALID_CONTEXT;
     JSContext *cx = JSD_GetDefaultJSContext (mCx);
-#ifdef GC_MARK_DEBUG
-    FILE *file = fopen("jsds-roots.txt", "w");
-    js_DumpGCHeap = file;
-#endif
     JS_GC(cx);
-#ifdef GC_MARK_DEBUG
-    if (file)
-        fclose (file);
-    js_DumpGCHeap = NULL;
-#endif
     return NS_OK;
 }
     
 NS_IMETHODIMP
+jsdService::DumpHeap(const char* fileName)
+{
+    ASSERT_VALID_CONTEXT;
+#ifndef DEBUG
+    return NS_ERROR_NOT_IMPLEMENTED;
+#else
+    nsresult rv = NS_OK;
+    FILE *file = fileName ? fopen(fileName, "w") : stdout;
+    if (!file) {
+        rv = NS_ERROR_FAILURE;
+    } else {
+        JSContext *cx = JSD_GetDefaultJSContext (mCx);
+        if (!JS_DumpHeap(cx, NULL, 0, NULL, (size_t)-1, NULL,
+                         NS_REINTERPRET_CAST(JSPrintfFormater, &fprintf),
+                         file)) {
+            rv = NS_ERROR_FAILURE;
+        }
+        if (file != stdout)
+            fclose(file);
+    }
+    return rv;
+#endif
+}
+
+NS_IMETHODIMP
 jsdService::ClearProfileData ()
 {
     ASSERT_VALID_CONTEXT;
     JSD_ClearAllProfileData (mCx);
     return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/js/src/js.c
+++ b/js/src/js.c
@@ -1347,85 +1347,91 @@ DumpStats(JSContext *cx, JSObject *obj, 
         }
     }
     return JS_TRUE;
 }
 
 static JSBool
 DumpHeap(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
 {
-    jsval v = JSVAL_NULL;
     char *fileName = NULL;
-    size_t maxRecursionLevel = (size_t)-1;
+    void* startThing = NULL;
+    uint32 startTraceKind = 0;
+    void *thingToFind = NULL;
+    size_t maxDepth = (size_t)-1;
     void *thingToIgnore = NULL;
+    jsval *vp;
     FILE *dumpFile;
     JSBool ok;
-    JSTracer *trc;
-
-    if (argc != 0 && argv[0] != JSVAL_NULL && argv[0] != JSVAL_VOID) {
-        v = argv[0];
-        if (!JSVAL_IS_TRACEABLE(v)) {
-            fprintf(gErrFile,
-                    "dumpHeap: the first argument is not null or "
-                    "a heap-allocated thing\n");
-            return JS_FALSE;
-        }
-    }
-
-    if (argc > 1 && argv[1] != JSVAL_NULL && argv[1] != JSVAL_VOID) {
+
+    vp = &argv[0];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
         JSString *str;
 
-        str = JS_ValueToString(cx, argv[1]);
+        str = JS_ValueToString(cx, *vp);
         if (!str)
             return JS_FALSE;
-        argv[1] = STRING_TO_JSVAL(str);
+        *vp = STRING_TO_JSVAL(str);
         fileName = JS_GetStringBytes(str);
     }
 
-    if (argc > 2 && argv[2] != JSVAL_NULL && argv[2] != JSVAL_VOID) {
+    vp = &argv[1];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        startThing = JSVAL_TO_TRACEABLE(*vp);
+        startTraceKind = JSVAL_TRACE_KIND(*vp);
+    }
+
+    vp = &argv[2];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        thingToFind = JSVAL_TO_TRACEABLE(*vp);
+    }
+
+    vp = &argv[3];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
         uint32 depth;
 
-        if (!JS_ValueToECMAUint32(cx, argv[2], &depth))
+        if (!JS_ValueToECMAUint32(cx, *vp, &depth))
             return JS_FALSE;
-        maxRecursionLevel = depth;
+        maxDepth = depth;
     }
 
-    if (argc > 3 && argv[3] != JSVAL_NULL && argv[3] != JSVAL_VOID) {
-        if (JSVAL_IS_GCTHING(argv[3]))
-            thingToIgnore = JSVAL_TO_GCTHING(argv[3]);
+    vp = &argv[4];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        thingToIgnore = JSVAL_TO_TRACEABLE(*vp);
     }
 
     if (!fileName) {
         dumpFile = stdout;
     } else {
         dumpFile = fopen(fileName, "w");
         if (!dumpFile) {
             fprintf(gErrFile, "dumpHeap: can't open %s: %s\n",
                     fileName, strerror(errno));
             return JS_FALSE;
         }
     }
 
-    trc = js_NewGCHeapDumper(cx, NULL, dumpFile, maxRecursionLevel,
-                             thingToIgnore);
-    if (!trc) {
-        ok = JS_FALSE;
-    } else {
-        if (v == JSVAL_NULL) {
-            js_TraceRuntime(trc);
-        } else {
-            JS_TraceChildren(trc, JSVAL_TO_TRACEABLE(v), JSVAL_TRACE_KIND(v));
-        }
-        ok = js_FreeGCHeapDumper(trc);
-    }
-
+    ok = JS_DumpHeap(cx, startThing, startTraceKind, thingToFind,
+                     maxDepth, thingToIgnore,
+                     (JSPrintfFormater)fprintf, dumpFile);
     if (dumpFile != stdout)
         fclose(dumpFile);
-
     return ok;
+
+  not_traceable_arg:
+    fprintf(gErrFile,
+            "dumpHeap: argument %u is not null or a heap-allocated thing\n",
+            (unsigned)(vp - argv));
+    return JS_FALSE;
 }
 
 #endif /* DEBUG */
 
 #ifdef TEST_EXPORT
 static JSBool
 DoExport(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
 {
@@ -2198,17 +2204,17 @@ static JSFunctionSpec shell_functions[] 
     {"line2pc",         LineToPC,       0,0,0},
     {"pc2line",         PCToLine,       0,0,0},
     {"stringsAreUTF8",  StringsAreUTF8, 0,0,0},
     {"testUTF8",        TestUTF8,       1,0,0},
     {"throwError",      ThrowError,     0,0,0},
 #ifdef DEBUG
     {"dis",             Disassemble,    1,0,0},
     {"dissrc",          DisassWithSrc,  1,0,0},
-    {"dumpHeap",        DumpHeap,       3,0,0},
+    {"dumpHeap",        DumpHeap,       5,0,0},
     {"notes",           Notes,          1,0,0},
     {"tracing",         Tracing,        0,0,0},
     {"stats",           DumpStats,      1,0,0},
 #endif
 #ifdef TEST_EXPORT
     {"xport",           DoExport,       2,0,0},
 #endif
 #ifdef TEST_CVTARGS
@@ -2242,17 +2248,18 @@ static char *shell_help_messages[] = {
     "line2pc([fun,] line)   Map line number to PC",
     "pc2line(fun[, pc])     Map PC to line number",
     "stringsAreUTF8()       Check if strings are UTF-8 encoded",
     "testUTF8(mode)         Perform UTF-8 tests (modes are 1 to 4)",
     "throwError()           Throw an error from JS_ReportError",
 #ifdef DEBUG
     "dis([fun])             Disassemble functions into bytecodes",
     "dissrc([fun])          Disassemble functions with source lines",
-    "dumpHeap([obj])        Display reachable objects",
+    "dumpHeap([fileName], [start], [toFind], [maxDepth], [toIgnore])\n"
+    "                       Interface to JS_DumpHeap with output sent to file",
     "notes([fun])           Show source notes for functions",
     "tracing([toggle])      Turn tracing on or off",
     "stats([string ...])    Dump 'arena', 'atom', 'global' stats",
 #endif
 #ifdef TEST_EXPORT
     "xport(obj, id)         Export identified property from object",
 #endif
 #ifdef TEST_CVTARGS
--- a/js/src/jsapi.c
+++ b/js/src/jsapi.c
@@ -1859,16 +1859,474 @@ JS_UnlockGCThing(JSContext *cx, void *th
 
 JS_PUBLIC_API(JSBool)
 JS_UnlockGCThingRT(JSRuntime *rt, void *thing)
 {
     return js_UnlockGCThingRT(rt, thing);
 }
 
 JS_PUBLIC_API(void)
+JS_TraceRuntime(JSTracer *trc)
+{
+    JSBool allAtoms = trc->context->runtime->gcKeepAtoms != 0;
+
+    js_TraceRuntime(trc, allAtoms);
+}
+
+#ifdef DEBUG
+
+#ifdef HAVE_XPCONNECT
+#include "dump_xpc.h"
+#endif
+
+JS_PUBLIC_API(void)
+JS_PrintTraceThingInfo(char *buf, size_t bufsize, JSTracer *trc,
+                       void *thing, uint32 kind, JSBool details)
+{
+    const char *name;
+    size_t n;
+
+    if (bufsize == 0)
+        return;
+
+    switch (kind) {
+      case JSTRACE_OBJECT:
+      {
+        JSObject *obj = (JSObject *)thing;
+        JSClass *clasp = STOBJ_GET_CLASS(obj);
+
+        name = clasp->name;
+#ifdef HAVE_XPCONNECT
+        if (clasp->flags & JSCLASS_PRIVATE_IS_NSISUPPORTS) {
+            jsval privateValue = STOBJ_GET_SLOT(obj, JSSLOT_PRIVATE);
+
+            JS_ASSERT(clasp->flags & JSCLASS_HAS_PRIVATE);
+            if (!JSVAL_IS_VOID(privateValue)) {
+                void  *privateThing = JSVAL_TO_PRIVATE(privateValue);
+                const char *xpcClassName = GetXPCObjectClassName(privateThing);
+
+                if (xpcClassName)
+                    name = xpcClassName;
+            }
+        }
+#endif
+        break;
+      }
+
+      case JSTRACE_STRING:
+        name = JSSTRING_IS_DEPENDENT((JSString *)thing)
+               ? "substring"
+               : "string";
+        break;
+
+      case JSTRACE_DOUBLE:
+        name = "double";
+        break;
+
+      case JSTRACE_FUNCTION:
+        name = "function";
+        break;
+
+      case JSTRACE_ATOM:
+        name = "atom";
+        break;
+
+#if JS_HAS_XML_SUPPORT
+      case JSTRACE_NAMESPACE:
+        name = "namespace";
+        break;
+
+      case JSTRACE_QNAME:
+        name = "qname";
+        break;
+
+      case JSTRACE_XML:
+        name = "xml";
+        break;
+#endif
+      default:
+        JS_ASSERT(0);
+        return;
+        break;
+    }
+
+    n = strlen(name);
+    if (n > bufsize - 1)
+        n = bufsize - 1;
+    memcpy(buf, name, n + 1);
+    buf += n;
+    bufsize -= n;
+
+    if (details && bufsize > 2) {
+        *buf++ = ' ';
+        bufsize--;
+
+        switch (kind) {
+          case JSTRACE_OBJECT:
+          {
+            JSObject  *obj = (JSObject *)thing;
+            jsval     privateValue = STOBJ_GET_SLOT(obj, JSSLOT_PRIVATE);
+            void      *privateThing = JSVAL_IS_VOID(privateValue)
+                                      ? NULL
+                                      : JSVAL_TO_PRIVATE(privateValue);
+
+            JS_snprintf(buf, bufsize, "%p", privateThing);
+            break;
+          }
+
+          case JSTRACE_STRING:
+            js_PutEscapedString(buf, bufsize, (JSString *)thing, 0);
+            break;
+
+          case JSTRACE_DOUBLE:
+            JS_snprintf(buf, bufsize, "%g", *(jsdouble *)thing);
+            break;
+
+          case JSTRACE_FUNCTION:
+          {
+            JSFunction *fun = (JSFunction *)thing;
+
+            if (fun->atom && ATOM_IS_STRING(fun->atom))
+                js_PutEscapedString(buf, bufsize, ATOM_TO_STRING(fun->atom), 0);
+            break;
+          }
+
+          case JSTRACE_ATOM:
+          {
+            JSAtom *atom = (JSAtom *)thing;
+
+            if (ATOM_IS_INT(atom))
+                JS_snprintf(buf, bufsize, "%d", ATOM_TO_INT(atom));
+            else if (ATOM_IS_STRING(atom))
+                js_PutEscapedString(buf, bufsize, ATOM_TO_STRING(atom), 0);
+            else
+                JS_snprintf(buf, bufsize, "object");
+            break;
+          }
+
+#if JS_HAS_XML_SUPPORT
+          case GCX_NAMESPACE:
+          {
+            JSXMLNamespace *ns = (JSXMLNamespace *)thing;
+
+            if (ns->prefix) {
+                n = js_PutEscapedString(buf, bufsize, ns->prefix, 0);
+                buf += n;
+                bufsize -= n;
+            }
+            if (bufsize > 2) {
+                *buf++ = ':';
+                bufsize--;
+                js_PutEscapedString(buf, bufsize, ns->uri, 0);
+            }
+            break;
+          }
+
+          case GCX_QNAME:
+          {
+            JSXMLQName *qn = (JSXMLQName *)thing;
+
+            if (qn->prefix) {
+                n = js_PutEscapedString(buf, bufsize, qn->prefix, 0);
+                buf += n;
+                bufsize -= n;
+            }
+            if (bufsize > 2) {
+                *buf++ = '(';
+                bufsize--;
+                n = js_PutEscapedString(buf, bufsize, qn->uri, 0);
+                buf += n;
+                bufsize -= n;
+                if (bufsize > 3) {
+                    *buf++ = ')';
+                    *buf++ = ':';
+                    bufsize -= 2;
+                    js_PutEscapedString(buf, bufsize, qn->localName, 0);
+                }
+            }
+            break;
+          }
+
+          case GCX_XML:
+          {
+            extern const char *js_xml_class_str[];
+            JSXML *xml = (JSXML *)thing;
+
+            JS_snprintf(buf, bufsize, "%s", js_xml_class_str[xml->xml_class]);
+            break;
+          }
+#endif
+          default:
+            JS_ASSERT(0);
+            break;
+        }
+    }
+    buf[bufsize - 1] = '\0';
+}
+
+typedef struct JSHeapDumpNode JSHeapDumpNode;
+
+struct JSHeapDumpNode {
+    void            *thing;
+    uint32          kind;
+    JSHeapDumpNode  *next;          /* next sibling */
+    JSHeapDumpNode  *parent;        /* node with the thing that refer to thing
+                                       from this node */
+    char            edgeName[1];    /* name of the edge from parent->thing
+                                       into thing */
+};
+
+typedef struct JSDumpingTracer {
+    JSTracer            base;
+    JSDHashTable        visited;
+    JSBool              ok;
+    void                *startThing;
+    void                *thingToFind;
+    void                *thingToIgnore;
+    JSHeapDumpNode      *parentNode;
+    JSHeapDumpNode      **lastNodep;
+    char                buffer[200];
+} JSDumpingTracer;
+
+static void
+DumpNotify(JSTracer *trc, void *thing, uint32 kind)
+{
+    JSDumpingTracer *dtrc;
+    JSContext *cx;
+    JSDHashEntryStub *entry;
+    JSHeapDumpNode *node;
+    const char *edgeName;
+    size_t edgeNameSize;
+
+    JS_ASSERT(trc->callback == DumpNotify);
+    dtrc = (JSDumpingTracer *)trc;
+
+    if (!dtrc->ok || thing == dtrc->thingToIgnore)
+        return;
+
+    cx = trc->context;
+
+    /*
+     * Check if we have already seen thing unless it is thingToFind to include
+     * it to the graph each time we reach it and print all live things that
+     * refer to thingToFind.
+     *
+     * This does not print all possible paths leading to thingToFind since
+     * when a thing A refers directly or indirectly to thingToFind and A is
+     * present several times in the graph, we will print only the first path
+     * leading to A and thingToFind, other ways to reach A will be ignored.
+     */
+    if (dtrc->thingToFind != thing) {
+        /*
+         * The startThing check allows to avoid putting startThing into the
+         * hash table before tracing startThing in JS_DumpHeap.
+         */
+        if (thing == dtrc->startThing)
+            return;
+        entry = (JSDHashEntryStub *)
+            JS_DHashTableOperate(&dtrc->visited, thing, JS_DHASH_ADD);
+        if (!entry) {
+            JS_ReportOutOfMemory(cx);
+            dtrc->ok = JS_FALSE;
+            return;
+        }
+        if (entry->key)
+            return;
+        entry->key = thing;
+    }
+
+    if (dtrc->base.debugPrinter) {
+        dtrc->base.debugPrinter(trc, dtrc->buffer, sizeof(dtrc->buffer));
+        edgeName = dtrc->buffer;
+    } else if (dtrc->base.debugPrintIndex != (size_t)-1) {
+        JS_snprintf(dtrc->buffer, sizeof(dtrc->buffer), "%s[%lu]",
+                    (const char *)dtrc->base.debugPrintArg,
+                    dtrc->base.debugPrintIndex);
+        edgeName = dtrc->buffer;
+    } else {
+        edgeName = (const char*)dtrc->base.debugPrintArg;
+    }
+
+    edgeNameSize = strlen(edgeName) + 1;
+    node = (JSHeapDumpNode *)
+        JS_malloc(cx, offsetof(JSHeapDumpNode, edgeName) + edgeNameSize);
+    if (!node) {
+        dtrc->ok = JS_FALSE;
+        return;
+    }
+
+    node->thing = thing;
+    node->kind = kind;
+    node->next = NULL;
+    node->parent = dtrc->parentNode;
+    memcpy(node->edgeName, edgeName, edgeNameSize);
+
+    JS_ASSERT(!*dtrc->lastNodep);
+    *dtrc->lastNodep = node;
+    dtrc->lastNodep = &node->next;
+}
+
+/* Dump node and the chain that leads to thing it contains. */
+static JSBool
+DumpNode(JSDumpingTracer *dtrc, JSHeapDumpNode *node,
+         JSPrintfFormater format, void *closure)
+{
+    JSHeapDumpNode *prev, *following;
+    size_t chainLimit;
+    JSBool ok;
+    enum { MAX_PARENTS_TO_PRINT = 10 };
+
+    JS_PrintTraceThingInfo(dtrc->buffer, sizeof dtrc->buffer,
+                           &dtrc->base, node->thing, node->kind, JS_TRUE);
+    if (format(closure, "%p %-22s via ", node->thing, dtrc->buffer) < 0)
+        return JS_FALSE;
+
+    /*
+     * We need to print the parent chain in the reverse order. To do it in
+     * O(N) time where N is the chain length we first reverse the chain while
+     * searching for the top and then print each node while restoring the
+     * chain order.
+     */
+    chainLimit = MAX_PARENTS_TO_PRINT;
+    prev = NULL;
+    for (;;) {
+        following = node->parent;
+        node->parent = prev;
+        prev = node;
+        node = following;
+        if (!node)
+            break;
+        if (chainLimit == 0) {
+            if (format(closure, "...") < 0)
+                return JS_FALSE;
+            break;
+        }
+        --chainLimit;
+    }
+
+    node = prev;
+    prev = following;
+    ok = JS_TRUE;
+    do {
+        /* Loop must continue even when !ok to restore the parent chain. */
+        if (ok) {
+            if (!prev) {
+                /* Print edge from some runtime root or startThing. */
+                if (format(closure, "%s", node->edgeName) < 0)
+                    ok = JS_FALSE;
+            } else {
+                JS_PrintTraceThingInfo(dtrc->buffer, sizeof dtrc->buffer,
+                                       &dtrc->base, prev->thing, prev->kind,
+                                       JS_FALSE);
+                if (format(closure, "(%p %s).%s",
+                           prev->thing, dtrc->buffer, node->edgeName) < 0) {
+                    ok = JS_FALSE;
+                }
+            }
+        }
+        following = node->parent;
+        node->parent = prev;
+        prev = node;
+        node = following;
+    } while (node);
+
+    return ok && format(closure, "\n") >= 0;
+}
+
+JS_PUBLIC_API(JSBool)
+JS_DumpHeap(JSContext *cx, void* startThing, uint32 startKind,
+            void *thingToFind, size_t maxDepth, void *thingToIgnore,
+            JSPrintfFormater format, void *closure)
+{
+    JSDumpingTracer dtrc;
+    JSHeapDumpNode *node, *children, *next, *parent;
+    size_t depth;
+    JSBool thingToFindWasTraced;
+
+    if (maxDepth == 0)
+        return JS_TRUE;
+
+    JS_TRACER_INIT(&dtrc.base, cx, DumpNotify);
+    if (!JS_DHashTableInit(&dtrc.visited, JS_DHashGetStubOps(),
+                           NULL, sizeof(JSDHashEntryStub),
+                           JS_DHASH_DEFAULT_CAPACITY(100))) {
+        JS_ReportOutOfMemory(cx);
+        return JS_FALSE;
+    }
+    dtrc.ok = JS_TRUE;
+    dtrc.startThing = startThing;
+    dtrc.thingToFind = thingToFind;
+    dtrc.thingToIgnore = thingToIgnore;
+    dtrc.parentNode = NULL;
+    node = NULL;
+    dtrc.lastNodep = &node;
+    if (!startThing) {
+        JS_ASSERT(startKind == 0);
+        JS_TraceRuntime(&dtrc.base);
+    } else {
+        JS_TraceChildren(&dtrc.base, startThing, startKind);
+    }
+
+    depth = 1;
+    if (!node)
+        goto dump_out;
+
+    thingToFindWasTraced = thingToFind && thingToFind == startThing;
+    for (;;) {
+        /*
+         * Loop must continue even when !dtrc.ok to free all nodes allocated
+         * so far.
+         */
+        if (dtrc.ok) {
+            if (thingToFind == NULL || thingToFind == node->thing)
+                dtrc.ok = DumpNode(&dtrc, node, format, closure);
+
+            /* Descend into children. */
+            if (dtrc.ok &&
+                depth < maxDepth &&
+                (thingToFind != node->thing || !thingToFindWasTraced)) {
+                dtrc.parentNode = node;
+                children = NULL;
+                dtrc.lastNodep = &children;
+                JS_TraceChildren(&dtrc.base, node->thing, node->kind);
+                if (thingToFind == node->thing)
+                    thingToFindWasTraced = JS_TRUE;
+                if (children != NULL) {
+                    ++depth;
+                    node = children;
+                    continue;
+                }
+            }
+        }
+
+        /* Move to next or parents next and free the node. */
+        for (;;) {
+            next = node->next;
+            parent = node->parent;
+            JS_free(cx, node);
+            node = next;
+            if (node)
+                break;
+            if (!parent)
+                goto dump_out;
+            JS_ASSERT(depth > 1);
+            --depth;
+            node = parent;
+        }
+    }
+
+  dump_out:
+    JS_ASSERT(depth == 1);
+    JS_DHashTableFinish(&dtrc.visited);
+    return dtrc.ok;
+}
+
+#endif /* DEBUG */
+
+JS_PUBLIC_API(void)
 JS_MarkGCThing(JSContext *cx, void *thing, const char *name, void *arg)
 {
     JSTracer *trc;
 
     trc = (JSTracer *)arg;
     if (!trc)
         trc = cx->runtime->gcMarkingTracer;
     else
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -905,17 +905,16 @@ struct JSTracer {
  * The method to call on each reference to a traceable thing storted in a
  * particular JSObject or other runtime structure. With DEBUG defined the
  * caller before calling JS_CallTracer must initialize JSTracer fields
  * describing the reference using the macros below.
  */
 extern JS_PUBLIC_API(void)
 JS_CallTracer(JSTracer *trc, void *thing, uint32 kind);
 
-
 /*
  * Set debugging information about a reference to a traceable thing to prepare
  * for the following call to JS_CallTracer.
  *
  * When printer is null, arg must be const char * or char * C string naming
  * the reference and index must be either (size_t)-1 indicating that the name
  * alone describes the reference or it must be an index into some array vector
  * that stores the reference.
@@ -1004,20 +1003,43 @@ JS_CallTracer(JSTracer *trc, void *thing
         (trc)->context = (cx_);                                               \
         (trc)->callback = (callback_);                                        \
         JS_SET_TRACING_DETAILS(trc, NULL, NULL, (size_t)-1);                  \
     JS_END_MACRO
 
 extern JS_PUBLIC_API(void)
 JS_TraceChildren(JSTracer *trc, void *thing, uint32 kind);
 
+extern JS_PUBLIC_API(void)
+JS_TraceRuntime(JSTracer *trc);
+
 #ifdef DEBUG
+
 extern JS_PUBLIC_API(void)
 JS_PrintTraceThingInfo(char *buf, size_t bufsize, JSTracer *trc,
                        void *thing, uint32 kind, JSBool includeDetails);
+/*
+ * DEBUG-only method to dump an object graph of heap-allocated things.
+ *
+ * start: when non-null, dump only things reachable from start thing. Otherwise
+ *        dump all things rechable from runtime roots.
+ * startKind: trace kind of start if start is not null. Must be 0 when start
+ *            is null.
+ * thingToFind: dump only paths in the object graph leading to thingToFind
+ *              when non-null.
+ * maxDepth: the upper bound on the number of edges to descend from the graph
+ *           roots.
+ * thingToIgnore: thing to ignore during graph traversal when non-null.
+ * format: callback to format the dump output.
+ * closure: an argument to pass to formater.
+ */
+extern JS_PUBLIC_API(JSBool)
+JS_DumpHeap(JSContext *cx, void* startThing, uint32 startKind,
+            void *thingToFind, size_t maxDepth, void *thingToIgnore,
+            JSPrintfFormater format, void *closure);
 #endif
 
 /*
  * Garbage collector API.
  */
 extern JS_PUBLIC_API(void)
 JS_GC(JSContext *cx);
 
--- a/js/src/jsatom.c
+++ b/js/src/jsatom.c
@@ -444,22 +444,24 @@ JS_STATIC_DLL_CALLBACK(intN)
 js_locked_atom_tracer(JSHashEntry *he, intN i, void *arg)
 {
     JSAtom *atom;
     TraceArgs *args;
 
     atom = (JSAtom *)he;
     args = (TraceArgs *)arg;
     if ((atom->flags & (ATOM_PINNED | ATOM_INTERNED)) || args->allAtoms) {
-        JS_CALL_TRACER(args->trc, atom, JSTRACE_ATOM,
-                       (atom->flags & ATOM_PINNED)
-                       ? "pinned_atom"
-                       : (atom->flags & ATOM_INTERNED)
-                       ? "interned_atom"
-                       : "locked_atom");
+        JS_SET_TRACING_INDEX(args->trc,
+                             (atom->flags & ATOM_PINNED)
+                             ? "pinned_atom"
+                             : (atom->flags & ATOM_INTERNED)
+                             ? "interned_atom"
+                             : "locked_atom",
+                             (size_t)i);
+        JS_CallTracer(args->trc, atom, JSTRACE_ATOM);
     }
     return HT_ENUMERATE_NEXT;
 }
 
 void
 js_TraceLockedAtoms(JSTracer *trc, JSBool allAtoms)
 {
     JSAtomState *state;
--- a/js/src/jsgc.c
+++ b/js/src/jsgc.c
@@ -1789,388 +1789,16 @@ js_UnlockGCThingRT(JSRuntime *rt, void *
 
     rt->gcPoke = JS_TRUE;
 out:
     METER(rt->gcStats.unlock++);
     JS_UNLOCK_GC(rt);
     return JS_TRUE;
 }
 
-#ifdef DEBUG
-
-#include <stdio.h>
-#include "jsprf.h"
-
-#ifdef HAVE_XPCONNECT
-#include "dump_xpc.h"
-#endif
-
-JS_PUBLIC_API(void)
-JS_PrintTraceThingInfo(char *buf, size_t bufsize, JSTracer *trc,
-                       void *thing, uint32 kind, JSBool details)
-{
-    const char *name;
-    size_t n;
-
-    if (bufsize == 0)
-        return;
-
-    switch (kind) {
-      case JSTRACE_OBJECT:
-      {
-        JSObject *obj = (JSObject *)thing;
-        JSClass *clasp = STOBJ_GET_CLASS(obj);
-
-        name = clasp->name;
-#ifdef HAVE_XPCONNECT
-        if (clasp->flags & JSCLASS_PRIVATE_IS_NSISUPPORTS) {
-            jsval privateValue = STOBJ_GET_SLOT(obj, JSSLOT_PRIVATE);
-
-            JS_ASSERT(clasp->flags & JSCLASS_HAS_PRIVATE);
-            if (!JSVAL_IS_VOID(privateValue)) {
-                void  *privateThing = JSVAL_TO_PRIVATE(privateValue);
-                const char *xpcClassName = GetXPCObjectClassName(privateThing);
-
-                if (xpcClassName)
-                    name = xpcClassName;
-            }
-        }
-#endif
-        break;
-      }
-
-      case JSTRACE_STRING:
-        name = JSSTRING_IS_DEPENDENT((JSString *)thing)
-               ? "substring"
-               : "string";
-        break;
-
-      case JSTRACE_DOUBLE:
-        name = "double";
-        break;
-
-      case JSTRACE_FUNCTION:
-        name = "function";
-        break;
-
-      case JSTRACE_ATOM:
-        name = "atom";
-        break;
-
-#if JS_HAS_XML_SUPPORT
-      case JSTRACE_NAMESPACE:
-        name = "namespace";
-        break;
-
-      case JSTRACE_QNAME:
-        name = "qname";
-        break;
-
-      case JSTRACE_XML:
-        name = "xml";
-        break;
-#endif
-      default:
-        JS_ASSERT(0);
-        return;
-        break;
-    }
-
-    n = strlen(name);
-    if (n > bufsize - 1)
-        n = bufsize - 1;
-    memcpy(buf, name, n + 1);
-    buf += n;
-    bufsize -= n;
-
-    if (details && bufsize > 2) {
-        *buf++ = ' ';
-        bufsize--;
-
-        switch (kind) {
-          case JSTRACE_OBJECT:
-          {
-            JSObject  *obj = (JSObject *)thing;
-            jsval     privateValue = STOBJ_GET_SLOT(obj, JSSLOT_PRIVATE);
-            void      *privateThing = JSVAL_IS_VOID(privateValue)
-                                      ? NULL
-                                      : JSVAL_TO_PRIVATE(privateValue);
-
-            JS_snprintf(buf, bufsize, "%8p", privateThing);
-            break;
-          }
-
-          case JSTRACE_STRING:
-            js_PutEscapedString(buf, bufsize, (JSString *)thing, 0);
-            break;
-
-          case JSTRACE_DOUBLE:
-            JS_snprintf(buf, bufsize, "%g", *(jsdouble *)thing);
-            break;
-
-          case JSTRACE_FUNCTION:
-          {
-            JSFunction *fun = (JSFunction *)thing;
-
-            if (fun->atom && ATOM_IS_STRING(fun->atom))
-                js_PutEscapedString(buf, bufsize, ATOM_TO_STRING(fun->atom), 0);
-            break;
-          }
-
-          case JSTRACE_ATOM:
-          {
-            JSAtom *atom = (JSAtom *)thing;
-
-            if (ATOM_IS_INT(atom))
-                JS_snprintf(buf, bufsize, "%d", ATOM_TO_INT(atom));
-            else if (ATOM_IS_STRING(atom))
-                js_PutEscapedString(buf, bufsize, ATOM_TO_STRING(atom), 0);
-            else
-                JS_snprintf(buf, bufsize, "object");
-            break;
-          }
-
-#if JS_HAS_XML_SUPPORT
-          case GCX_NAMESPACE:
-          {
-            JSXMLNamespace *ns = (JSXMLNamespace *)thing;
-
-            if (ns->prefix) {
-                n = js_PutEscapedString(buf, bufsize, ns->prefix, 0);
-                buf += n;
-                bufsize -= n;
-            }
-            if (bufsize > 2) {
-                *buf++ = ':';
-                bufsize--;
-                js_PutEscapedString(buf, bufsize, ns->uri, 0);
-            }
-            break;
-          }
-
-          case GCX_QNAME:
-          {
-            JSXMLQName *qn = (JSXMLQName *)thing;
-
-            if (qn->prefix) {
-                n = js_PutEscapedString(buf, bufsize, qn->prefix, 0);
-                buf += n;
-                bufsize -= n;
-            }
-            if (bufsize > 2) {
-                *buf++ = '(';
-                bufsize--;
-                n = js_PutEscapedString(buf, bufsize, qn->uri, 0);
-                buf += n;
-                bufsize -= n;
-                if (bufsize > 3) {
-                    *buf++ = ')';
-                    *buf++ = ':';
-                    bufsize -= 2;
-                    js_PutEscapedString(buf, bufsize, qn->localName, 0);
-                }
-            }
-            break;
-          }
-
-          case GCX_XML:
-          {
-            extern const char *js_xml_class_str[];
-            JSXML *xml = (JSXML *)thing;
-
-            JS_snprintf(buf, bufsize, "%s", js_xml_class_str[xml->xml_class]);
-            break;
-          }
-#endif
-          default:
-            JS_ASSERT(0);
-            break;
-        }
-    }
-    buf[bufsize - 1] = '\0';
-}
-
-typedef struct JSGCDumpNode JSGCDumpNode;
-
-struct JSGCDumpNode {
-    void            *thing;
-    uint32          kind;
-    char            *name;
-    JSGCDumpNode    *next;
-};
-
-typedef struct JSDumpingTracer
-{
-    JSTracer        base;
-    JSDHashTable    visited;
-    JSBool          ok;
-    FILE            *file;
-    void            *thingToFind;
-    size_t          maxRecursionDepth;
-    void            *thingToIgnore;
-    size_t          recursionDepth;
-    JSGCDumpNode    *top, **bottomp;
-    char            buffer[200];
-} JSDumpingTracer;
-
-static void
-DumpThing(JSDumpingTracer *dtrc, JSGCDumpNode *node)
-{
-    FILE *fp;
-
-    fp = dtrc->file;
-    fprintf(fp, "%p ", node->thing);
-    JS_PrintTraceThingInfo(dtrc->buffer, sizeof(dtrc->buffer),
-                           &dtrc->base, node->thing, node->kind, JS_TRUE);
-    fputs(dtrc->buffer, fp);
-
-    /* Print path to this node from a traversal root. */
-    node = dtrc->top;
-    if (node->next) {
-        fputs("\tvia ", fp);
-        do {
-            if (node != dtrc->top)
-                fputc('.', fp);
-            fprintf(fp, "%s(%p ", node->name, node->thing);
-            JS_PrintTraceThingInfo(dtrc->buffer, sizeof(dtrc->buffer),
-                                   &dtrc->base, node->thing, node->kind,
-                                   JS_FALSE);
-            fputs(dtrc->buffer, fp);
-            node = node->next;
-        } while (node->next);
-    }
-    fputc('\n', fp);
-}
-
-static void
-DumpNotify(JSTracer *trc, void *thing, uint32 kind)
-{
-    JSDumpingTracer *dtrc;
-    JSContext *cx;
-    JSDHashEntryStub *entry;
-    JSGCDumpNode node, **bottomp;
-    const char *name;
-
-    JS_ASSERT(trc->callback == DumpNotify);
-    dtrc = (JSDumpingTracer *)trc;
-
-    if (!dtrc->ok)
-        return;
-
-    if (dtrc->thingToIgnore == thing)
-        return;
-
-    cx = trc->context;
-    entry = (JSDHashEntryStub *)
-        JS_DHashTableOperate(&dtrc->visited, thing, JS_DHASH_ADD);
-    if (!entry) {
-        JS_ReportOutOfMemory(cx);
-        dtrc->ok = JS_FALSE;
-        return;
-    }
-    if (entry->key)
-        return;
-    entry->key = thing;
-
-    node.thing = thing;
-    node.kind = kind;
-    if (dtrc->base.debugPrinter) {
-        dtrc->base.debugPrinter(trc, dtrc->buffer, sizeof(dtrc->buffer));
-        name = dtrc->buffer;
-    } else if (dtrc->base.debugPrintIndex != (size_t)-1) {
-        JS_snprintf(dtrc->buffer, sizeof(dtrc->buffer), "%s[%lu]",
-                    (const char *)dtrc->base.debugPrintArg,
-                    dtrc->base.debugPrintIndex);
-        name = dtrc->buffer;
-    } else {
-        name = (const char*)dtrc->base.debugPrintArg;
-    }
-
-    node.name = JS_strdup(cx, name);
-    if (!node.name) {
-        dtrc->ok = JS_FALSE;
-        return;
-    }
-
-    node.next  = NULL;
-    bottomp = dtrc->bottomp;
-    JS_ASSERT(!*bottomp);
-    *bottomp = &node;
-    dtrc->bottomp = &node.next;
-
-    /*
-     * Dump thingToFind each time we reach it during the tracing to print all
-     * live references to the thing.
-     */
-    if (!dtrc->thingToFind || dtrc->thingToFind == thing)
-        DumpThing(dtrc, &node);
-
-    if (dtrc->recursionDepth < dtrc->maxRecursionDepth) {
-        dtrc->recursionDepth++;
-        JS_TraceChildren(&dtrc->base, thing, kind);
-        dtrc->recursionDepth--;
-    }
-
-    *bottomp = NULL;
-    dtrc->bottomp = bottomp;
-    JS_free(cx, node.name);
-}
-
-JS_FRIEND_API(JSTracer *)
-js_NewGCHeapDumper(JSContext *cx, void *thingToFind, FILE *fp,
-                   size_t maxRecursionDepth, void *thingToIgnore)
-{
-    JSDumpingTracer *dtrc;
-
-    dtrc = (JSDumpingTracer *)JS_malloc(cx, sizeof(*dtrc));
-    if (!dtrc)
-        return NULL;
-
-    JS_TRACER_INIT(&dtrc->base, cx, DumpNotify);
-    if (!JS_DHashTableInit(&dtrc->visited, JS_DHashGetStubOps(),
-                           NULL, sizeof(JSDHashEntryStub),
-                           JS_DHASH_DEFAULT_CAPACITY(100))) {
-        JS_ReportOutOfMemory(cx);
-        JS_free(cx, dtrc);
-        return NULL;
-    }
-
-    dtrc->ok = JS_TRUE;
-    dtrc->file = fp ? fp : stderr;
-    dtrc->thingToFind = thingToFind;
-    dtrc->maxRecursionDepth = maxRecursionDepth;
-    dtrc->thingToIgnore = thingToIgnore;
-    dtrc->recursionDepth = 0;
-    dtrc->top = NULL;
-    dtrc->bottomp = &dtrc->top;
-
-    return &dtrc->base;
-}
-
-JS_FRIEND_API(JSBool)
-js_FreeGCHeapDumper(JSTracer *trc)
-{
-    JSDumpingTracer *dtrc;
-    JSBool ok;
-    JSContext *cx;
-
-    JS_ASSERT(trc->callback == DumpNotify);
-    dtrc = (JSDumpingTracer *)trc;
-    JS_ASSERT(!dtrc->top);
-    JS_ASSERT(dtrc->bottomp == &dtrc->top);
-
-    JS_DHashTableFinish(&dtrc->visited);
-    ok = dtrc->ok;
-    cx = dtrc->base.context;
-    JS_free(cx, dtrc);
-    return ok;
-}
-
-#endif /* DEBUG */
-
 JS_PUBLIC_API(void)
 JS_TraceChildren(JSTracer *trc, void *thing, uint32 kind)
 {
     JSObject *obj;
     size_t nslots, i;
     jsval v;
     JSString *str;
 
@@ -2684,21 +2312,21 @@ gc_lock_traversal(JSDHashTable *table, J
         }                                                                     \
     JS_END_MACRO
 
 void
 js_TraceStackFrame(JSTracer *trc, JSStackFrame *fp)
 {
     uintN depth, nslots;
     if (fp->callobj)
-        JS_CALL_OBJECT_TRACER(trc, fp->callobj, "call object");
+        JS_CALL_OBJECT_TRACER(trc, fp->callobj, "call");
     if (fp->argsobj)
-        JS_CALL_OBJECT_TRACER(trc, fp->argsobj, "arguments object");
+        JS_CALL_OBJECT_TRACER(trc, fp->argsobj, "arguments");
     if (fp->varobj)
-        JS_CALL_OBJECT_TRACER(trc, fp->varobj, "variables object");
+        JS_CALL_OBJECT_TRACER(trc, fp->varobj, "variables");
     if (fp->script) {
         js_TraceScript(trc, fp->script);
         if (fp->spbase) {
             /*
              * Don't mark what has not been pushed yet, or what has been
              * popped already.
              */
             depth = fp->script->depth;
@@ -2857,18 +2485,18 @@ js_TraceContext(JSTracer *trc, JSContext
             TRACE_JSVALS(trc, tvr->count, tvr->u.array, "tvr->u.array");
         }
     }
 
     if (acx->sharpObjectMap.depth > 0)
         js_TraceSharpMap(trc, &acx->sharpObjectMap);
 }
 
-static void
-TraceRuntime(JSTracer *trc, JSBool allAtoms)
+void
+js_TraceRuntime(JSTracer *trc, JSBool allAtoms)
 {
     JSRuntime *rt = trc->context->runtime;
     JSContext *iter, *acx;
 
     JS_DHashTableEnumerate(&rt->gcRootsHash, gc_root_traversal, trc);
     if (rt->gcLocksHash)
         JS_DHashTableEnumerate(rt->gcLocksHash, gc_lock_traversal, trc);
     js_TraceLockedAtoms(trc, allAtoms);
@@ -2879,22 +2507,16 @@ TraceRuntime(JSTracer *trc, JSBool allAt
     TraceGeneratorsToClose(trc);
 #endif
 
     iter = NULL;
     while ((acx = js_ContextIterator(rt, JS_TRUE, &iter)) != NULL)
         js_TraceContext(trc, acx);
 }
 
-JS_FRIEND_API(void)
-js_TraceRuntime(JSTracer *trc)
-{
-    TraceRuntime(trc, JS_FALSE);
-}
-
 /*
  * When gckind is GC_LAST_DITCH, it indicates a call from js_NewGCThing with
  * rt->gcLock already held and when the lock should be kept on return.
  */
 void
 js_GC(JSContext *cx, JSGCInvocationKind gckind)
 {
     JSRuntime *rt;
@@ -3105,17 +2727,17 @@ restart:
     JS_ASSERT(rt->gcUnscannedBagSize == 0);
 
     /*
      * Mark phase.
      */
     JS_TRACER_INIT(&trc, cx, NULL);
     rt->gcMarkingTracer = &trc;
     JS_ASSERT(IS_GC_MARKING_TRACER(&trc));
-    TraceRuntime(&trc, keepAtoms);
+    js_TraceRuntime(&trc, keepAtoms);
     js_MarkScriptFilenames(rt, keepAtoms);
 
     /*
      * Mark children of things that caused too deep recursion during the above
      * tracing.
      */
     ScanDelayedChildren(&trc);
 
--- a/js/src/jsgc.h
+++ b/js/src/jsgc.h
@@ -202,27 +202,16 @@ extern JSBool
 js_IsAboutToBeFinalized(JSContext *cx, void *thing);
 
 /*
  * Macro to test if a traversal is the marking phase of GC to avoid exposing
  * JSAtom and ScriptFilenameEntry to traversal implementations.
  */
 #define IS_GC_MARKING_TRACER(trc) ((trc)->callback == NULL)
 
-#ifdef DEBUG
-
-extern JS_FRIEND_API(JSTracer *)
-js_NewGCHeapDumper(JSContext *cx, void *thingToFind, FILE *fp,
-                   size_t maxRecursionDepth, void *thingToIgnore);
-
-extern JS_FRIEND_API(JSBool)
-js_FreeGCHeapDumper(JSTracer *trc);
-
-#endif
-
 JS_STATIC_ASSERT(JSTRACE_STRING == 2);
 
 #define JSTRACE_FUNCTION    3
 #define JSTRACE_ATOM        4
 #define JSTRACE_NAMESPACE   5
 #define JSTRACE_QNAME       6
 #define JSTRACE_XML         7
 
@@ -238,18 +227,18 @@ JS_STATIC_ASSERT(JSTRACE_STRING == 2);
  * behind v.
  */
 extern void
 js_CallValueTracerIfGCThing(JSTracer *trc, jsval v);
 
 extern void
 js_TraceStackFrame(JSTracer *trc, JSStackFrame *fp);
 
-extern JS_FRIEND_API(void)
-js_TraceRuntime(JSTracer *trc);
+extern void
+js_TraceRuntime(JSTracer *trc, JSBool allAtoms);
 
 extern JS_FRIEND_API(void)
 js_TraceContext(JSTracer *trc, JSContext *acx);
 
 /*
  * Kinds of js_GC invocation.
  */
 typedef enum JSGCInvocationKind {
--- a/js/src/jspubtd.h
+++ b/js/src/jspubtd.h
@@ -710,11 +710,23 @@ typedef JSBool
  * a window object in the DOM level 0).  If there are no principals associated
  * with obj, return null.  Therefore null does not mean an error was reported;
  * in no event should an error be reported or an exception be thrown by this
  * callback's implementation.
  */
 typedef JSPrincipals *
 (* JS_DLL_CALLBACK JSObjectPrincipalsFinder)(JSContext *cx, JSObject *obj);
 
+/*
+ * Output formated arguments as specified by format string. See fprintf/sprintf
+ * documentation for specification of format. Return the number of characters
+ * printed or -1 if an error occur.
+ */
+typedef int
+(* JS_DLL_CALLBACK JSPrintfFormater)(void *closure, const char *format, ...)
+#if defined __GNUC__
+    __attribute__ ((format (printf, 2, 3)))
+#endif
+;
+
 JS_END_EXTERN_C
 
 #endif /* jspubtd_h___ */
--- a/js/src/xpconnect/shell/xpcshell.cpp
+++ b/js/src/xpconnect/shell/xpcshell.cpp
@@ -289,61 +289,123 @@ DumpXPC(JSContext *cx, JSObject *obj, ui
     if(xpc)
         xpc->DebugDump((int16)depth);
     return JS_TRUE;
 }
 
 /* XXX needed only by GC() */
 #include "jscntxt.h"
 
-#ifdef GC_MARK_DEBUG
-extern "C" JS_FRIEND_DATA(FILE *) js_DumpGCHeap;
-#endif
-
 JS_STATIC_DLL_CALLBACK(JSBool)
 GC(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
 {
     JSRuntime *rt;
     uint32 preBytes;
 
     rt = cx->runtime;
     preBytes = rt->gcBytes;
-#ifdef GC_MARK_DEBUG
-    if (argc && JSVAL_IS_STRING(argv[0])) {
-        char *name = JS_GetStringBytes(JSVAL_TO_STRING(argv[0]));
-        FILE *file = fopen(name, "w");
-        if (!file) {
-            fprintf(gErrFile, "gc: can't open %s: %s\n", strerror(errno));
-            return JS_FALSE;
-        }
-        js_DumpGCHeap = file;
-    } else {
-        js_DumpGCHeap = stdout;
-    }
-#endif
     JS_GC(cx);
-#ifdef GC_MARK_DEBUG
-    if (js_DumpGCHeap != stdout)
-        fclose(js_DumpGCHeap);
-    js_DumpGCHeap = NULL;
-#endif
     fprintf(gOutFile, "before %lu, after %lu, break %08lx\n",
            (unsigned long)preBytes, (unsigned long)rt->gcBytes,
 #ifdef XP_UNIX
            (unsigned long)sbrk(0)
 #else
            0
 #endif
            );
 #ifdef JS_GCMETER
     js_DumpGCStats(rt, stdout);
 #endif
     return JS_TRUE;
 }
 
+#ifdef DEBUG
+
+static JSBool
+DumpHeap(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
+{
+    char *fileName = NULL;
+    void* startThing = NULL;
+    uint32 startTraceKind = 0;
+    void *thingToFind = NULL;
+    size_t maxDepth = (size_t)-1;
+    void *thingToIgnore = NULL;
+    jsval *vp;
+    FILE *dumpFile;
+    JSBool ok;
+
+    vp = &argv[0];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        JSString *str;
+
+        str = JS_ValueToString(cx, *vp);
+        if (!str)
+            return JS_FALSE;
+        *vp = STRING_TO_JSVAL(str);
+        fileName = JS_GetStringBytes(str);
+    }
+
+    vp = &argv[1];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        startThing = JSVAL_TO_TRACEABLE(*vp);
+        startTraceKind = JSVAL_TRACE_KIND(*vp);
+    }
+
+    vp = &argv[2];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        thingToFind = JSVAL_TO_TRACEABLE(*vp);
+    }
+
+    vp = &argv[3];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        uint32 depth;
+
+        if (!JS_ValueToECMAUint32(cx, *vp, &depth))
+            return JS_FALSE;
+        maxDepth = depth;
+    }
+
+    vp = &argv[4];
+    if (*vp != JSVAL_NULL && *vp != JSVAL_VOID) {
+        if (!JSVAL_IS_TRACEABLE(*vp))
+            goto not_traceable_arg;
+        thingToIgnore = JSVAL_TO_TRACEABLE(*vp);
+    }
+
+    if (!fileName) {
+        dumpFile = gOutFile;
+    } else {
+        dumpFile = fopen(fileName, "w");
+        if (!dumpFile) {
+            fprintf(gErrFile, "dumpHeap: can't open %s: %s\n",
+                    fileName, strerror(errno));
+            return JS_FALSE;
+        }
+    }
+
+    ok = JS_DumpHeap(cx, startThing, startTraceKind, thingToFind,
+                     maxDepth, thingToIgnore,
+                     (JSPrintfFormater)fprintf, dumpFile);
+    if (dumpFile != gOutFile)
+        fclose(dumpFile);
+    return ok;
+
+  not_traceable_arg:
+    fprintf(gErrFile,
+            "dumpHeap: argument %u is not null or a heap-allocated thing\n",
+            (unsigned)(vp - argv));
+    return JS_FALSE;
+}
+
+#endif /* DEBUG */
+
 JS_STATIC_DLL_CALLBACK(JSBool)
 Clear(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
 {
     if (argc > 0 && !JSVAL_IS_PRIMITIVE(argv[0])) {
         JS_ClearScope(cx, JSVAL_TO_OBJECT(argv[0]));
     } else {
         JS_ReportError(cx, "'clear' requires an object");
         return JS_FALSE;
@@ -356,16 +418,19 @@ static JSFunctionSpec glob_functions[] =
     {"load",            Load,           1,0,0},
     {"quit",            Quit,           0,0,0},
     {"version",         Version,        1,0,0},
     {"build",           BuildDate,      0,0,0},
     {"dumpXPC",         DumpXPC,        1,0,0},
     {"dump",            Dump,           1,0,0},
     {"gc",              GC,             0,0,0},
     {"clear",           Clear,          1,0,0},
+#ifdef DEBUG
+    {"dumpHeap",        DumpHeap,       5,0,0},
+#endif
     {nsnull,nsnull,0,0,0}
 };
 
 JSClass global_class = {
     "global", 0,
     JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,
     JS_EnumerateStub, JS_ResolveStub,   JS_ConvertStub,   JS_FinalizeStub
 };