michael@0: /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- michael@0: * michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #ifdef HAVE_GETOPT_H michael@0: #include michael@0: #else michael@0: #ifdef XP_WIN michael@0: #include "getopt.c" michael@0: #else michael@0: extern int getopt(int argc, char *const *argv, const char *shortopts); michael@0: extern char *optarg; michael@0: extern int optind; michael@0: #endif michael@0: #endif michael@0: #include michael@0: #include michael@0: #include michael@0: #include "prlog.h" michael@0: #include "prprf.h" michael@0: #include "plhash.h" michael@0: #include "pldhash.h" michael@0: #include "nsTraceMalloc.h" michael@0: #include "tmreader.h" michael@0: michael@0: static char *program; michael@0: static int sort_by_direct = 0; michael@0: static int js_mode = 0; michael@0: static int do_tree_dump = 0; michael@0: static int unified_output = 0; michael@0: static char *function_dump = NULL; michael@0: static uint32_t min_subtotal = 0; michael@0: michael@0: static void compute_callsite_totals(tmcallsite *site) michael@0: { michael@0: tmcallsite *kid; michael@0: michael@0: site->allocs.bytes.total += site->allocs.bytes.direct; michael@0: site->allocs.calls.total += site->allocs.calls.direct; michael@0: for (kid = site->kids; kid; kid = kid->siblings) { michael@0: compute_callsite_totals(kid); michael@0: site->allocs.bytes.total += kid->allocs.bytes.total; michael@0: site->allocs.calls.total += kid->allocs.calls.total; michael@0: } michael@0: } michael@0: michael@0: static void walk_callsite_tree(tmcallsite *site, int level, int kidnum, FILE *fp) michael@0: { michael@0: tmcallsite *parent; michael@0: tmgraphnode *comp, *pcomp, *lib, *plib; michael@0: tmmethodnode *meth, *pmeth; michael@0: int old_meth_low, old_comp_low, old_lib_low, nkids; michael@0: tmcallsite *kid; michael@0: michael@0: parent = site->parent; michael@0: comp = lib = NULL; michael@0: meth = NULL; michael@0: if (parent) { michael@0: meth = site->method; michael@0: if (meth) { michael@0: pmeth = parent->method; michael@0: if (pmeth && pmeth != meth) { michael@0: if (!meth->graphnode.low) { michael@0: meth->graphnode.allocs.bytes.total += site->allocs.bytes.total; michael@0: meth->graphnode.allocs.calls.total += site->allocs.calls.total; michael@0: } michael@0: if (!tmgraphnode_connect(&(pmeth->graphnode), &(meth->graphnode), site)) michael@0: goto bad; michael@0: michael@0: comp = meth->graphnode.up; michael@0: if (comp) { michael@0: pcomp = pmeth->graphnode.up; michael@0: if (pcomp && pcomp != comp) { michael@0: if (!comp->low) { michael@0: comp->allocs.bytes.total michael@0: += site->allocs.bytes.total; michael@0: comp->allocs.calls.total michael@0: += site->allocs.calls.total; michael@0: } michael@0: if (!tmgraphnode_connect(pcomp, comp, site)) michael@0: goto bad; michael@0: michael@0: lib = comp->up; michael@0: if (lib) { michael@0: plib = pcomp->up; michael@0: if (plib && plib != lib) { michael@0: if (!lib->low) { michael@0: lib->allocs.bytes.total michael@0: += site->allocs.bytes.total; michael@0: lib->allocs.calls.total michael@0: += site->allocs.calls.total; michael@0: } michael@0: if (!tmgraphnode_connect(plib, lib, site)) michael@0: goto bad; michael@0: } michael@0: old_lib_low = lib->low; michael@0: if (!old_lib_low) michael@0: lib->low = level; michael@0: } michael@0: } michael@0: old_comp_low = comp->low; michael@0: if (!old_comp_low) michael@0: comp->low = level; michael@0: } michael@0: } michael@0: old_meth_low = meth->graphnode.low; michael@0: if (!old_meth_low) michael@0: meth->graphnode.low = level; michael@0: } michael@0: } michael@0: michael@0: if (do_tree_dump) { michael@0: fprintf(fp, "%c%*s%3d %3d %s %lu %ld\n", michael@0: site->kids ? '+' : '-', level, "", level, kidnum, michael@0: meth ? tmmethodnode_name(meth) : "???", michael@0: (unsigned long)site->allocs.bytes.direct, michael@0: (long)site->allocs.bytes.total); michael@0: } michael@0: nkids = 0; michael@0: level++; michael@0: for (kid = site->kids; kid; kid = kid->siblings) { michael@0: walk_callsite_tree(kid, level, nkids, fp); michael@0: nkids++; michael@0: } michael@0: michael@0: if (meth) { michael@0: if (!old_meth_low) michael@0: meth->graphnode.low = 0; michael@0: if (comp) { michael@0: if (!old_comp_low) michael@0: comp->low = 0; michael@0: if (lib) { michael@0: if (!old_lib_low) michael@0: lib->low = 0; michael@0: } michael@0: } michael@0: } michael@0: return; michael@0: michael@0: bad: michael@0: perror(program); michael@0: exit(1); michael@0: } michael@0: michael@0: /* michael@0: * Linked list bubble-sort (waterson and brendan went bald hacking this). michael@0: * michael@0: * Sort the list in non-increasing order, using the expression passed as the michael@0: * 'lessthan' formal macro parameter. This expression should use 'curr' as michael@0: * the pointer to the current node (of type nodetype) and 'next' as the next michael@0: * node pointer. It should return true if curr is less than next, and false michael@0: * otherwise. michael@0: */ michael@0: #define BUBBLE_SORT_LINKED_LIST(listp, nodetype, lessthan) \ michael@0: PR_BEGIN_MACRO \ michael@0: nodetype *curr, **currp, *next, **nextp, *tmp; \ michael@0: \ michael@0: currp = listp; \ michael@0: while ((curr = *currp) != NULL && curr->next) { \ michael@0: nextp = &curr->next; \ michael@0: while ((next = *nextp) != NULL) { \ michael@0: if (lessthan) { \ michael@0: tmp = curr->next; \ michael@0: *currp = tmp; \ michael@0: if (tmp == next) { \ michael@0: PR_ASSERT(nextp == &curr->next); \ michael@0: curr->next = next->next; \ michael@0: next->next = curr; \ michael@0: } else { \ michael@0: *nextp = next->next; \ michael@0: curr->next = next->next; \ michael@0: next->next = tmp; \ michael@0: *currp = next; \ michael@0: *nextp = curr; \ michael@0: nextp = &curr->next; \ michael@0: } \ michael@0: curr = next; \ michael@0: continue; \ michael@0: } \ michael@0: nextp = &next->next; \ michael@0: } \ michael@0: currp = &curr->next; \ michael@0: } \ michael@0: PR_END_MACRO michael@0: michael@0: static int tabulate_node(PLHashEntry *he, int i, void *arg) michael@0: { michael@0: tmgraphnode *node = (tmgraphnode*) he; michael@0: tmgraphnode **table = (tmgraphnode**) arg; michael@0: michael@0: table[i] = node; michael@0: BUBBLE_SORT_LINKED_LIST(&node->down, tmgraphnode, michael@0: (curr->allocs.bytes.total < next->allocs.bytes.total)); michael@0: return HT_ENUMERATE_NEXT; michael@0: } michael@0: michael@0: /* Sort in reverse size order, so biggest node comes first. */ michael@0: static int node_table_compare(const void *p1, const void *p2) michael@0: { michael@0: const tmgraphnode *node1, *node2; michael@0: uint32_t key1, key2; michael@0: michael@0: node1 = *(const tmgraphnode**) p1; michael@0: node2 = *(const tmgraphnode**) p2; michael@0: if (sort_by_direct) { michael@0: key1 = node1->allocs.bytes.direct; michael@0: key2 = node2->allocs.bytes.direct; michael@0: } else { michael@0: key1 = node1->allocs.bytes.total; michael@0: key2 = node2->allocs.bytes.total; michael@0: } michael@0: return (key2 < key1) ? -1 : (key2 > key1) ? 1 : 0; michael@0: } michael@0: michael@0: static int mean_size_compare(const void *p1, const void *p2) michael@0: { michael@0: const tmgraphnode *node1, *node2; michael@0: double div1, div2, key1, key2; michael@0: michael@0: node1 = *(const tmgraphnode**) p1; michael@0: node2 = *(const tmgraphnode**) p2; michael@0: div1 = (double)node1->allocs.calls.direct; michael@0: div2 = (double)node2->allocs.calls.direct; michael@0: if (div1 == 0 || div2 == 0) michael@0: return (int)(div2 - div1); michael@0: key1 = (double)node1->allocs.bytes.direct / div1; michael@0: key2 = (double)node2->allocs.bytes.direct / div2; michael@0: if (key1 < key2) michael@0: return 1; michael@0: if (key1 > key2) michael@0: return -1; michael@0: return 0; michael@0: } michael@0: michael@0: static const char *prettybig(uint32_t num, char *buf, size_t limit) michael@0: { michael@0: if (num >= 1000000000) michael@0: PR_snprintf(buf, limit, "%1.2fG", (double) num / 1e9); michael@0: else if (num >= 1000000) michael@0: PR_snprintf(buf, limit, "%1.2fM", (double) num / 1e6); michael@0: else if (num >= 1000) michael@0: PR_snprintf(buf, limit, "%1.2fK", (double) num / 1e3); michael@0: else michael@0: PR_snprintf(buf, limit, "%lu", (unsigned long) num); michael@0: return buf; michael@0: } michael@0: michael@0: static double percent(uint32_t num, uint32_t total) michael@0: { michael@0: if (num == 0) michael@0: return 0.0; michael@0: return ((double) num * 100) / (double) total; michael@0: } michael@0: michael@0: static void sort_graphlink_list(tmgraphlink **listp, int which) michael@0: { michael@0: BUBBLE_SORT_LINKED_LIST(listp, tmgraphlink, michael@0: (TM_LINK_TO_EDGE(curr, which)->allocs.bytes.total michael@0: < TM_LINK_TO_EDGE(next, which)->allocs.bytes.total)); michael@0: } michael@0: michael@0: static void dump_graphlink_list(tmgraphlink *list, int which, const char *name, michael@0: FILE *fp) michael@0: { michael@0: tmcounts bytes; michael@0: tmgraphlink *link; michael@0: tmgraphedge *edge; michael@0: char buf[16]; michael@0: michael@0: bytes.direct = bytes.total = 0; michael@0: for (link = list; link; link = link->next) { michael@0: edge = TM_LINK_TO_EDGE(link, which); michael@0: bytes.direct += edge->allocs.bytes.direct; michael@0: bytes.total += edge->allocs.bytes.total; michael@0: } michael@0: michael@0: if (js_mode) { michael@0: fprintf(fp, michael@0: " %s:{dbytes:%ld, tbytes:%ld, edges:[\n", michael@0: name, (long) bytes.direct, (long) bytes.total); michael@0: for (link = list; link; link = link->next) { michael@0: edge = TM_LINK_TO_EDGE(link, which); michael@0: fprintf(fp, michael@0: " {node:%d, dbytes:%ld, tbytes:%ld},\n", michael@0: link->node->sort, michael@0: (long) edge->allocs.bytes.direct, michael@0: (long) edge->allocs.bytes.total); michael@0: } michael@0: fputs(" ]},\n", fp); michael@0: } else { michael@0: fputs("", fp); michael@0: for (link = list; link; link = link->next) { michael@0: edge = TM_LINK_TO_EDGE(link, which); michael@0: fprintf(fp, michael@0: "%s (%1.2f%%)\n", michael@0: tmgraphnode_name(link->node), michael@0: prettybig(edge->allocs.bytes.total, buf, sizeof buf), michael@0: percent(edge->allocs.bytes.total, bytes.total)); michael@0: } michael@0: fputs("", fp); michael@0: } michael@0: } michael@0: michael@0: static void dump_graph(tmreader *tmr, PLHashTable *hashtbl, const char *varname, michael@0: const char *title, FILE *fp) michael@0: { michael@0: uint32_t i, count; michael@0: tmgraphnode **table, *node; michael@0: char *name; michael@0: size_t namelen; michael@0: char buf1[16], buf2[16], buf3[16], buf4[16]; michael@0: michael@0: count = hashtbl->nentries; michael@0: table = (tmgraphnode**) malloc(count * sizeof(tmgraphnode*)); michael@0: if (!table) { michael@0: perror(program); michael@0: exit(1); michael@0: } michael@0: PL_HashTableEnumerateEntries(hashtbl, tabulate_node, table); michael@0: qsort(table, count, sizeof(tmgraphnode*), node_table_compare); michael@0: for (i = 0; i < count; i++) michael@0: table[i]->sort = i; michael@0: michael@0: if (js_mode) { michael@0: fprintf(fp, michael@0: "var %s = {\n name:'%s', title:'%s', nodes:[\n", michael@0: varname, varname, title); michael@0: } else { michael@0: fprintf(fp, michael@0: "\n" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "\n", michael@0: title); michael@0: } michael@0: michael@0: for (i = 0; i < count; i++) { michael@0: /* Don't bother with truly puny nodes. */ michael@0: node = table[i]; michael@0: if (node->allocs.bytes.total < min_subtotal) michael@0: break; michael@0: michael@0: name = tmgraphnode_name(node); michael@0: if (js_mode) { michael@0: fprintf(fp, michael@0: " {name:'%s', dbytes:%ld, tbytes:%ld," michael@0: " dallocs:%ld, tallocs:%ld,\n", michael@0: name, michael@0: (long) node->allocs.bytes.direct, michael@0: (long) node->allocs.bytes.total, michael@0: (long) node->allocs.calls.direct, michael@0: (long) node->allocs.calls.total); michael@0: } else { michael@0: namelen = strlen(name); michael@0: fprintf(fp, michael@0: "" michael@0: "", michael@0: name, michael@0: (namelen > 40) ? 40 : (int)namelen, name, michael@0: (namelen > 40) ? "..." : ""); michael@0: if (node->down) { michael@0: fprintf(fp, michael@0: "", michael@0: tmgraphnode_name(node->down)); michael@0: } else { michael@0: fputs("", fp); michael@0: } michael@0: if (node->next) { michael@0: fprintf(fp, michael@0: "", michael@0: tmgraphnode_name(node->next)); michael@0: } else { michael@0: fputs("", fp); michael@0: } michael@0: fprintf(fp, michael@0: "" michael@0: "", michael@0: prettybig(node->allocs.bytes.total, buf1, sizeof buf1), michael@0: prettybig(node->allocs.bytes.direct, buf2, sizeof buf2), michael@0: percent(node->allocs.bytes.total, michael@0: tmr->calltree_root.allocs.bytes.total), michael@0: percent(node->allocs.bytes.direct, michael@0: tmr->calltree_root.allocs.bytes.total), michael@0: prettybig(node->allocs.calls.total, buf3, sizeof buf3), michael@0: prettybig(node->allocs.calls.direct, buf4, sizeof buf4), michael@0: percent(node->allocs.calls.total, michael@0: tmr->calltree_root.allocs.calls.total), michael@0: percent(node->allocs.calls.direct, michael@0: tmr->calltree_root.allocs.calls.total)); michael@0: } michael@0: michael@0: /* NB: we must use 'fin' because 'in' is a JS keyword! */ michael@0: sort_graphlink_list(&node->in, TM_EDGE_IN_LINK); michael@0: dump_graphlink_list(node->in, TM_EDGE_IN_LINK, "fin", fp); michael@0: sort_graphlink_list(&node->out, TM_EDGE_OUT_LINK); michael@0: dump_graphlink_list(node->out, TM_EDGE_OUT_LINK, "out", fp); michael@0: michael@0: if (js_mode) michael@0: fputs(" },\n", fp); michael@0: else michael@0: fputs("\n", fp); michael@0: } michael@0: michael@0: if (js_mode) { michael@0: fputs("]};\n", fp); michael@0: } else { michael@0: fputs("
%sDownNextTotal/Direct (percents)AllocationsFan-inFan-out
%.*s%sdownnext%s/%s (%1.2f%%/%1.2f%%)%s/%s (%1.2f%%/%1.2f%%)
\n
\n", fp); michael@0: michael@0: qsort(table, count, sizeof(tmgraphnode*), mean_size_compare); michael@0: michael@0: fprintf(fp, michael@0: "\n" michael@0: "\n" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "\n", michael@0: title); michael@0: michael@0: for (i = 0; i < count; i++) { michael@0: double allocs, bytes, mean, variance, sigma; michael@0: michael@0: node = table[i]; michael@0: allocs = (double)node->allocs.calls.direct; michael@0: if (!allocs) michael@0: continue; michael@0: michael@0: /* Compute direct-size mean and standard deviation. */ michael@0: bytes = (double)node->allocs.bytes.direct; michael@0: mean = bytes / allocs; michael@0: variance = allocs * node->sqsum - bytes * bytes; michael@0: if (variance < 0 || allocs == 1) michael@0: variance = 0; michael@0: else michael@0: variance /= allocs * (allocs - 1); michael@0: sigma = sqrt(variance); michael@0: michael@0: name = tmgraphnode_name(node); michael@0: namelen = strlen(name); michael@0: fprintf(fp, michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "" michael@0: "\n", michael@0: (namelen > 65) ? 45 : (int)namelen, name, michael@0: (namelen > 65) ? "..." : "", michael@0: prettybig((uint32_t)mean, buf1, sizeof buf1), michael@0: prettybig((uint32_t)sigma, buf2, sizeof buf2), michael@0: prettybig(node->allocs.calls.direct, buf3, sizeof buf3)); michael@0: } michael@0: fputs("
Direct Allocators
%sMean SizeStdDevAllocations" michael@0: "
%.*s%s%s%s%s
\n", fp); michael@0: } michael@0: michael@0: free((void*) table); michael@0: } michael@0: michael@0: static void my_tmevent_handler(tmreader *tmr, tmevent *event) michael@0: { michael@0: switch (event->type) { michael@0: case TM_EVENT_STATS: michael@0: if (js_mode) michael@0: break; michael@0: fprintf(stdout, michael@0: "

" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "\n" michael@0: "
CounterValue
maximum actual stack depth%lu
maximum callsite tree depth%lu
number of parent callsites%lu
maximum kids per parent%lu
hits looking for a kid%lu
misses looking for a kid%lu
steps over other kids%lu
callsite recurrences%lu
number of stack backtraces%lu
backtrace failures%lu
backtrace malloc failures%lu
backtrace dladdr failures%lu
malloc calls%lu
malloc failures%lu
calloc calls%lu
calloc failures%lu
realloc calls%lu
realloc failures%lu
free calls%lu
free(null) calls%lu
", michael@0: (unsigned long) event->u.stats.tmstats.calltree_maxstack, michael@0: (unsigned long) event->u.stats.tmstats.calltree_maxdepth, michael@0: (unsigned long) event->u.stats.tmstats.calltree_parents, michael@0: (unsigned long) event->u.stats.tmstats.calltree_maxkids, michael@0: (unsigned long) event->u.stats.tmstats.calltree_kidhits, michael@0: (unsigned long) event->u.stats.tmstats.calltree_kidmisses, michael@0: (unsigned long) event->u.stats.tmstats.calltree_kidsteps, michael@0: (unsigned long) event->u.stats.tmstats.callsite_recurrences, michael@0: (unsigned long) event->u.stats.tmstats.backtrace_calls, michael@0: (unsigned long) event->u.stats.tmstats.backtrace_failures, michael@0: (unsigned long) event->u.stats.tmstats.btmalloc_failures, michael@0: (unsigned long) event->u.stats.tmstats.dladdr_failures, michael@0: (unsigned long) event->u.stats.tmstats.malloc_calls, michael@0: (unsigned long) event->u.stats.tmstats.malloc_failures, michael@0: (unsigned long) event->u.stats.tmstats.calloc_calls, michael@0: (unsigned long) event->u.stats.tmstats.calloc_failures, michael@0: (unsigned long) event->u.stats.tmstats.realloc_calls, michael@0: (unsigned long) event->u.stats.tmstats.realloc_failures, michael@0: (unsigned long) event->u.stats.tmstats.free_calls, michael@0: (unsigned long) event->u.stats.tmstats.null_free_calls); michael@0: michael@0: if (event->u.stats.calltree_maxkids_parent) { michael@0: tmcallsite *site = michael@0: tmreader_callsite(tmr, event->u.stats.calltree_maxkids_parent); michael@0: if (site && site->method) { michael@0: fprintf(stdout, "

callsite with the most kids: %s

", michael@0: tmmethodnode_name(site->method)); michael@0: } michael@0: } michael@0: michael@0: if (event->u.stats.calltree_maxstack_top) { michael@0: tmcallsite *site = michael@0: tmreader_callsite(tmr, event->u.stats.calltree_maxstack_top); michael@0: fputs("

deepest callsite tree path:\n" michael@0: "\n" michael@0: "\n", michael@0: stdout); michael@0: while (site) { michael@0: fprintf(stdout, michael@0: "\n", michael@0: site->method ? tmmethodnode_name(site->method) : "???", michael@0: (unsigned long) site->offset); michael@0: site = site->parent; michael@0: } michael@0: fputs("
MethodOffset
%s0x%08lX
\n


\n", stdout); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: int main(int argc, char **argv) michael@0: { michael@0: int c, i, j, rv; michael@0: tmreader *tmr; michael@0: FILE *fp; michael@0: michael@0: program = *argv; michael@0: tmr = tmreader_new(program, NULL); michael@0: if (!tmr) { michael@0: perror(program); michael@0: exit(1); michael@0: } michael@0: michael@0: while ((c = getopt(argc, argv, "djtuf:m:")) != EOF) { michael@0: switch (c) { michael@0: case 'd': michael@0: sort_by_direct = 1; michael@0: break; michael@0: case 'j': michael@0: js_mode = 1; michael@0: break; michael@0: case 't': michael@0: do_tree_dump = 1; michael@0: break; michael@0: case 'u': michael@0: unified_output = 1; michael@0: break; michael@0: case 'f': michael@0: function_dump = optarg; michael@0: break; michael@0: case 'm': michael@0: min_subtotal = atoi(optarg); michael@0: break; michael@0: default: michael@0: fprintf(stderr, michael@0: "usage: %s [-dtu] [-f function-dump-filename] [-m min] [output.html]\n", michael@0: program); michael@0: exit(2); michael@0: } michael@0: } michael@0: michael@0: if (!js_mode) { michael@0: time_t start = time(NULL); michael@0: michael@0: fprintf(stdout, michael@0: "\n"); michael@0: fprintf(stdout, "%s starting at %s", program, ctime(&start)); michael@0: fflush(stdout); michael@0: } michael@0: michael@0: argc -= optind; michael@0: argv += optind; michael@0: if (argc == 0) { michael@0: if (tmreader_eventloop(tmr, "-", my_tmevent_handler) <= 0) michael@0: exit(1); michael@0: } else { michael@0: for (i = j = 0; i < argc; i++) { michael@0: fp = fopen(argv[i], "r"); michael@0: if (!fp) { michael@0: fprintf(stderr, "%s: can't open %s: %s\n", michael@0: program, argv[i], strerror(errno)); michael@0: exit(1); michael@0: } michael@0: rv = tmreader_eventloop(tmr, argv[i], my_tmevent_handler); michael@0: if (rv < 0) michael@0: exit(1); michael@0: if (rv > 0) michael@0: j++; michael@0: fclose(fp); michael@0: } michael@0: if (j == 0) michael@0: exit(1); michael@0: } michael@0: michael@0: compute_callsite_totals(&tmr->calltree_root); michael@0: walk_callsite_tree(&tmr->calltree_root, 0, 0, stdout); michael@0: michael@0: if (js_mode) { michael@0: fprintf(stdout, michael@0: "\n" michael@0: "\n" michael@0: " \n" michael@0: " \n" michael@0: "\n", michael@0: stdout); michael@0: } michael@0: } michael@0: michael@0: exit(0); michael@0: }