e6c7cdec06
This includes regenerating a bunch of files in opcodes/ with trunk cgen. gprof/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * basic_blocks.c: Update old style function definitions. * cg_arcs.c: Likewise. * cg_print.c: Likewise. * gen-c-prog.awk: Likewise. * gmon_io.c: Likewise. * hertz.c: Likewise. * hist.c: Likewise. * sym_ids.c: Likewise. bfd/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * cache.c: Update old style function definitions. * elf32-m68k.c: Likewise. * elf64-mmix.c: Likewise. * stab-syms.c: Likewise. opcodes/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * alpha-dis.c: Regenerate. * crx-dis.c: Likewise. * disassemble.c: Likewise. * epiphany-opc.c: Likewise. * fr30-opc.c: Likewise. * frv-opc.c: Likewise. * ip2k-opc.c: Likewise. * iq2000-opc.c: Likewise. * lm32-opc.c: Likewise. * lm32-opinst.c: Likewise. * m32c-opc.c: Likewise. * m32r-opc.c: Likewise. * m32r-opinst.c: Likewise. * mep-opc.c: Likewise. * mt-opc.c: Likewise. * or1k-opc.c: Likewise. * or1k-opinst.c: Likewise. * tic80-opc.c: Likewise. * xc16x-opc.c: Likewise. * xstormy16-opc.c: Likewise. ld/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * emultempl/scoreelf.em: Likewise. binutils/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * resres.c: Likewise. gas/ChangeLog: 2016-04-20 Trevor Saunders <tbsaunde+binutils@tbsaunde.org> * cgen.c: Likewise. * config/tc-bfin.c: Likewise. * config/tc-ia64.c: Likewise. * config/tc-mep.c: Likewise. * config/tc-metag.c: Likewise. * config/tc-nios2.c: Likewise. * config/tc-rl78.c: Likewise.
1291 lines
34 KiB
C
1291 lines
34 KiB
C
/* cg_print.c - Print routines for displaying call graphs.
|
||
|
||
Copyright (C) 2000-2016 Free Software Foundation, Inc.
|
||
|
||
This file is part of GNU Binutils.
|
||
|
||
This program is free software; you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation; either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program; if not, write to the Free Software
|
||
Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA
|
||
02110-1301, USA. */
|
||
|
||
#include "gprof.h"
|
||
#include "libiberty.h"
|
||
#include "filenames.h"
|
||
#include "search_list.h"
|
||
#include "source.h"
|
||
#include "symtab.h"
|
||
#include "cg_arcs.h"
|
||
#include "cg_print.h"
|
||
#include "hist.h"
|
||
#include "utils.h"
|
||
#include "corefile.h"
|
||
|
||
/* Return value of comparison functions used to sort tables. */
|
||
#define LESSTHAN -1
|
||
#define EQUALTO 0
|
||
#define GREATERTHAN 1
|
||
|
||
static void print_header (void);
|
||
static void print_cycle (Sym *);
|
||
static int cmp_member (Sym *, Sym *);
|
||
static void sort_members (Sym *);
|
||
static void print_members (Sym *);
|
||
static int cmp_arc (Arc *, Arc *);
|
||
static void sort_parents (Sym *);
|
||
static void print_parents (Sym *);
|
||
static void sort_children (Sym *);
|
||
static void print_children (Sym *);
|
||
static void print_line (Sym *);
|
||
static int cmp_name (const PTR, const PTR);
|
||
static int cmp_arc_count (const PTR, const PTR);
|
||
static int cmp_fun_nuses (const PTR, const PTR);
|
||
static void order_and_dump_functions_by_arcs
|
||
(Arc **, unsigned long, int, Arc **, unsigned long *);
|
||
|
||
/* Declarations of automatically generated functions to output blurbs. */
|
||
extern void bsd_callg_blurb (FILE * fp);
|
||
extern void fsf_callg_blurb (FILE * fp);
|
||
|
||
double print_time = 0.0;
|
||
|
||
|
||
static void
|
||
print_header (void)
|
||
{
|
||
if (first_output)
|
||
first_output = FALSE;
|
||
else
|
||
printf ("\f\n");
|
||
|
||
if (!bsd_style_output)
|
||
{
|
||
if (print_descriptions)
|
||
printf (_("\t\t Call graph (explanation follows)\n\n"));
|
||
else
|
||
printf (_("\t\t\tCall graph\n\n"));
|
||
}
|
||
|
||
printf (_("\ngranularity: each sample hit covers %ld byte(s)"),
|
||
(long) hist_scale * (long) sizeof (UNIT));
|
||
|
||
if (print_time > 0.0)
|
||
printf (_(" for %.2f%% of %.2f seconds\n\n"),
|
||
100.0 / print_time, print_time / hz);
|
||
else
|
||
{
|
||
printf (_(" no time propagated\n\n"));
|
||
|
||
/* This doesn't hurt, since all the numerators will be 0.0. */
|
||
print_time = 1.0;
|
||
}
|
||
|
||
if (bsd_style_output)
|
||
{
|
||
printf ("%6.6s %5.5s %7.7s %11.11s %7.7s/%-7.7s %-8.8s\n",
|
||
"", "", "", "", _("called"), _("total"), _("parents"));
|
||
printf ("%-6.6s %5.5s %7.7s %11.11s %7.7s+%-7.7s %-8.8s\t%5.5s\n",
|
||
_("index"),
|
||
/* xgettext:no-c-format */
|
||
_("%time"),
|
||
_("self"), _("descendants"), _("called"), _("self"),
|
||
_("name"), _("index"));
|
||
printf ("%6.6s %5.5s %7.7s %11.11s %7.7s/%-7.7s %-8.8s\n",
|
||
"", "", "", "", _("called"), _("total"), _("children"));
|
||
printf ("\n");
|
||
}
|
||
else
|
||
{
|
||
printf (_("index %% time self children called name\n"));
|
||
}
|
||
}
|
||
|
||
/* Print a cycle header. */
|
||
|
||
static void
|
||
print_cycle (Sym *cyc)
|
||
{
|
||
char buf[BUFSIZ];
|
||
|
||
sprintf (buf, "[%d]", cyc->cg.index);
|
||
printf (bsd_style_output
|
||
? "%-6.6s %5.1f %7.2f %11.2f %7lu"
|
||
: "%-6.6s %5.1f %7.2f %7.2f %7lu", buf,
|
||
100 * (cyc->cg.prop.self + cyc->cg.prop.child) / print_time,
|
||
cyc->cg.prop.self / hz, cyc->cg.prop.child / hz, cyc->ncalls);
|
||
|
||
if (cyc->cg.self_calls != 0)
|
||
printf ("+%-7lu", cyc->cg.self_calls);
|
||
else
|
||
printf (" %7.7s", "");
|
||
|
||
printf (_(" <cycle %d as a whole> [%d]\n"), cyc->cg.cyc.num, cyc->cg.index);
|
||
}
|
||
|
||
/* Compare LEFT and RIGHT membmer. Major comparison key is
|
||
CG.PROP.SELF+CG.PROP.CHILD, secondary key is NCALLS+CG.SELF_CALLS. */
|
||
|
||
static int
|
||
cmp_member (Sym *left, Sym *right)
|
||
{
|
||
double left_time = left->cg.prop.self + left->cg.prop.child;
|
||
double right_time = right->cg.prop.self + right->cg.prop.child;
|
||
unsigned long left_calls = left->ncalls + left->cg.self_calls;
|
||
unsigned long right_calls = right->ncalls + right->cg.self_calls;
|
||
|
||
if (left_time > right_time)
|
||
return GREATERTHAN;
|
||
|
||
if (left_time < right_time)
|
||
return LESSTHAN;
|
||
|
||
if (left_calls > right_calls)
|
||
return GREATERTHAN;
|
||
|
||
if (left_calls < right_calls)
|
||
return LESSTHAN;
|
||
|
||
return EQUALTO;
|
||
}
|
||
|
||
/* Sort members of a cycle. */
|
||
|
||
static void
|
||
sort_members (Sym *cyc)
|
||
{
|
||
Sym *todo, *doing, *prev;
|
||
|
||
/* Detach cycle members from cyclehead,
|
||
and insertion sort them back on. */
|
||
todo = cyc->cg.cyc.next;
|
||
cyc->cg.cyc.next = 0;
|
||
|
||
for (doing = todo; doing != NULL; doing = todo)
|
||
{
|
||
todo = doing->cg.cyc.next;
|
||
|
||
for (prev = cyc; prev->cg.cyc.next; prev = prev->cg.cyc.next)
|
||
{
|
||
if (cmp_member (doing, prev->cg.cyc.next) == GREATERTHAN)
|
||
break;
|
||
}
|
||
|
||
doing->cg.cyc.next = prev->cg.cyc.next;
|
||
prev->cg.cyc.next = doing;
|
||
}
|
||
}
|
||
|
||
/* Print the members of a cycle. */
|
||
|
||
static void
|
||
print_members (Sym *cyc)
|
||
{
|
||
Sym *member;
|
||
|
||
sort_members (cyc);
|
||
|
||
for (member = cyc->cg.cyc.next; member; member = member->cg.cyc.next)
|
||
{
|
||
printf (bsd_style_output
|
||
? "%6.6s %5.5s %7.2f %11.2f %7lu"
|
||
: "%6.6s %5.5s %7.2f %7.2f %7lu",
|
||
"", "", member->cg.prop.self / hz, member->cg.prop.child / hz,
|
||
member->ncalls);
|
||
|
||
if (member->cg.self_calls != 0)
|
||
printf ("+%-7lu", member->cg.self_calls);
|
||
else
|
||
printf (" %7.7s", "");
|
||
|
||
printf (" ");
|
||
print_name (member);
|
||
printf ("\n");
|
||
}
|
||
}
|
||
|
||
/* Compare two arcs to/from the same child/parent.
|
||
- if one arc is a self arc, it's least.
|
||
- if one arc is within a cycle, it's less than.
|
||
- if both arcs are within a cycle, compare arc counts.
|
||
- if neither arc is within a cycle, compare with
|
||
time + child_time as major key
|
||
arc count as minor key. */
|
||
|
||
static int
|
||
cmp_arc (Arc *left, Arc *right)
|
||
{
|
||
Sym *left_parent = left->parent;
|
||
Sym *left_child = left->child;
|
||
Sym *right_parent = right->parent;
|
||
Sym *right_child = right->child;
|
||
double left_time, right_time;
|
||
|
||
DBG (TIMEDEBUG,
|
||
printf ("[cmp_arc] ");
|
||
print_name (left_parent);
|
||
printf (" calls ");
|
||
print_name (left_child);
|
||
printf (" %f + %f %lu/%lu\n", left->time, left->child_time,
|
||
left->count, left_child->ncalls);
|
||
printf ("[cmp_arc] ");
|
||
print_name (right_parent);
|
||
printf (" calls ");
|
||
print_name (right_child);
|
||
printf (" %f + %f %lu/%lu\n", right->time, right->child_time,
|
||
right->count, right_child->ncalls);
|
||
printf ("\n");
|
||
);
|
||
|
||
if (left_parent == left_child)
|
||
return LESSTHAN; /* Left is a self call. */
|
||
|
||
if (right_parent == right_child)
|
||
return GREATERTHAN; /* Right is a self call. */
|
||
|
||
if (left_parent->cg.cyc.num != 0 && left_child->cg.cyc.num != 0
|
||
&& left_parent->cg.cyc.num == left_child->cg.cyc.num)
|
||
{
|
||
/* Left is a call within a cycle. */
|
||
if (right_parent->cg.cyc.num != 0 && right_child->cg.cyc.num != 0
|
||
&& right_parent->cg.cyc.num == right_child->cg.cyc.num)
|
||
{
|
||
/* Right is a call within the cycle, too. */
|
||
if (left->count < right->count)
|
||
return LESSTHAN;
|
||
|
||
if (left->count > right->count)
|
||
return GREATERTHAN;
|
||
|
||
return EQUALTO;
|
||
}
|
||
else
|
||
{
|
||
/* Right isn't a call within the cycle. */
|
||
return LESSTHAN;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* Left isn't a call within a cycle. */
|
||
if (right_parent->cg.cyc.num != 0 && right_child->cg.cyc.num != 0
|
||
&& right_parent->cg.cyc.num == right_child->cg.cyc.num)
|
||
{
|
||
/* Right is a call within a cycle. */
|
||
return GREATERTHAN;
|
||
}
|
||
else
|
||
{
|
||
/* Neither is a call within a cycle. */
|
||
left_time = left->time + left->child_time;
|
||
right_time = right->time + right->child_time;
|
||
|
||
if (left_time < right_time)
|
||
return LESSTHAN;
|
||
|
||
if (left_time > right_time)
|
||
return GREATERTHAN;
|
||
|
||
if (left->count < right->count)
|
||
return LESSTHAN;
|
||
|
||
if (left->count > right->count)
|
||
return GREATERTHAN;
|
||
|
||
return EQUALTO;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
static void
|
||
sort_parents (Sym * child)
|
||
{
|
||
Arc *arc, *detached, sorted, *prev;
|
||
|
||
/* Unlink parents from child, then insertion sort back on to
|
||
sorted's parents.
|
||
*arc the arc you have detached and are inserting.
|
||
*detached the rest of the arcs to be sorted.
|
||
sorted arc list onto which you insertion sort.
|
||
*prev arc before the arc you are comparing. */
|
||
sorted.next_parent = 0;
|
||
|
||
for (arc = child->cg.parents; arc; arc = detached)
|
||
{
|
||
detached = arc->next_parent;
|
||
|
||
/* Consider *arc as disconnected; insert it into sorted. */
|
||
for (prev = &sorted; prev->next_parent; prev = prev->next_parent)
|
||
{
|
||
if (cmp_arc (arc, prev->next_parent) != GREATERTHAN)
|
||
break;
|
||
}
|
||
|
||
arc->next_parent = prev->next_parent;
|
||
prev->next_parent = arc;
|
||
}
|
||
|
||
/* Reattach sorted arcs to child. */
|
||
child->cg.parents = sorted.next_parent;
|
||
}
|
||
|
||
|
||
static void
|
||
print_parents (Sym *child)
|
||
{
|
||
Sym *parent;
|
||
Arc *arc;
|
||
Sym *cycle_head;
|
||
|
||
if (child->cg.cyc.head != 0)
|
||
cycle_head = child->cg.cyc.head;
|
||
else
|
||
cycle_head = child;
|
||
|
||
if (!child->cg.parents)
|
||
{
|
||
printf (bsd_style_output
|
||
? _("%6.6s %5.5s %7.7s %11.11s %7.7s %7.7s <spontaneous>\n")
|
||
: _("%6.6s %5.5s %7.7s %7.7s %7.7s %7.7s <spontaneous>\n"),
|
||
"", "", "", "", "", "");
|
||
return;
|
||
}
|
||
|
||
sort_parents (child);
|
||
|
||
for (arc = child->cg.parents; arc; arc = arc->next_parent)
|
||
{
|
||
parent = arc->parent;
|
||
if (child == parent || (child->cg.cyc.num != 0
|
||
&& parent->cg.cyc.num == child->cg.cyc.num))
|
||
{
|
||
/* Selfcall or call among siblings. */
|
||
printf (bsd_style_output
|
||
? "%6.6s %5.5s %7.7s %11.11s %7lu %7.7s "
|
||
: "%6.6s %5.5s %7.7s %7.7s %7lu %7.7s ",
|
||
"", "", "", "",
|
||
arc->count, "");
|
||
print_name (parent);
|
||
printf ("\n");
|
||
}
|
||
else
|
||
{
|
||
/* Regular parent of child. */
|
||
printf (bsd_style_output
|
||
? "%6.6s %5.5s %7.2f %11.2f %7lu/%-7lu "
|
||
: "%6.6s %5.5s %7.2f %7.2f %7lu/%-7lu ",
|
||
"", "",
|
||
arc->time / hz, arc->child_time / hz,
|
||
arc->count, cycle_head->ncalls);
|
||
print_name (parent);
|
||
printf ("\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
static void
|
||
sort_children (Sym *parent)
|
||
{
|
||
Arc *arc, *detached, sorted, *prev;
|
||
|
||
/* Unlink children from parent, then insertion sort back on to
|
||
sorted's children.
|
||
*arc the arc you have detached and are inserting.
|
||
*detached the rest of the arcs to be sorted.
|
||
sorted arc list onto which you insertion sort.
|
||
*prev arc before the arc you are comparing. */
|
||
sorted.next_child = 0;
|
||
|
||
for (arc = parent->cg.children; arc; arc = detached)
|
||
{
|
||
detached = arc->next_child;
|
||
|
||
/* Consider *arc as disconnected; insert it into sorted. */
|
||
for (prev = &sorted; prev->next_child; prev = prev->next_child)
|
||
{
|
||
if (cmp_arc (arc, prev->next_child) != LESSTHAN)
|
||
break;
|
||
}
|
||
|
||
arc->next_child = prev->next_child;
|
||
prev->next_child = arc;
|
||
}
|
||
|
||
/* Reattach sorted children to parent. */
|
||
parent->cg.children = sorted.next_child;
|
||
}
|
||
|
||
|
||
static void
|
||
print_children (Sym *parent)
|
||
{
|
||
Sym *child;
|
||
Arc *arc;
|
||
|
||
sort_children (parent);
|
||
arc = parent->cg.children;
|
||
|
||
for (arc = parent->cg.children; arc; arc = arc->next_child)
|
||
{
|
||
child = arc->child;
|
||
if (child == parent || (child->cg.cyc.num != 0
|
||
&& child->cg.cyc.num == parent->cg.cyc.num))
|
||
{
|
||
/* Self call or call to sibling. */
|
||
printf (bsd_style_output
|
||
? "%6.6s %5.5s %7.7s %11.11s %7lu %7.7s "
|
||
: "%6.6s %5.5s %7.7s %7.7s %7lu %7.7s ",
|
||
"", "", "", "", arc->count, "");
|
||
print_name (child);
|
||
printf ("\n");
|
||
}
|
||
else
|
||
{
|
||
/* Regular child of parent. */
|
||
printf (bsd_style_output
|
||
? "%6.6s %5.5s %7.2f %11.2f %7lu/%-7lu "
|
||
: "%6.6s %5.5s %7.2f %7.2f %7lu/%-7lu ",
|
||
"", "",
|
||
arc->time / hz, arc->child_time / hz,
|
||
arc->count, child->cg.cyc.head->ncalls);
|
||
print_name (child);
|
||
printf ("\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
static void
|
||
print_line (Sym *np)
|
||
{
|
||
char buf[BUFSIZ];
|
||
|
||
sprintf (buf, "[%d]", np->cg.index);
|
||
printf (bsd_style_output
|
||
? "%-6.6s %5.1f %7.2f %11.2f"
|
||
: "%-6.6s %5.1f %7.2f %7.2f", buf,
|
||
100 * (np->cg.prop.self + np->cg.prop.child) / print_time,
|
||
np->cg.prop.self / hz, np->cg.prop.child / hz);
|
||
|
||
if ((np->ncalls + np->cg.self_calls) != 0)
|
||
{
|
||
printf (" %7lu", np->ncalls);
|
||
|
||
if (np->cg.self_calls != 0)
|
||
printf ("+%-7lu ", np->cg.self_calls);
|
||
else
|
||
printf (" %7.7s ", "");
|
||
}
|
||
else
|
||
{
|
||
printf (" %7.7s %7.7s ", "", "");
|
||
}
|
||
|
||
print_name (np);
|
||
printf ("\n");
|
||
}
|
||
|
||
|
||
/* Print dynamic call graph. */
|
||
|
||
void
|
||
cg_print (Sym ** timesortsym)
|
||
{
|
||
unsigned int sym_index;
|
||
Sym *parent;
|
||
|
||
if (print_descriptions && bsd_style_output)
|
||
bsd_callg_blurb (stdout);
|
||
|
||
print_header ();
|
||
|
||
for (sym_index = 0; sym_index < symtab.len + num_cycles; ++sym_index)
|
||
{
|
||
parent = timesortsym[sym_index];
|
||
|
||
if ((ignore_zeros && parent->ncalls == 0
|
||
&& parent->cg.self_calls == 0 && parent->cg.prop.self == 0
|
||
&& parent->cg.prop.child == 0)
|
||
|| !parent->cg.print_flag
|
||
|| (line_granularity && ! parent->is_func))
|
||
continue;
|
||
|
||
if (!parent->name && parent->cg.cyc.num != 0)
|
||
{
|
||
/* Cycle header. */
|
||
print_cycle (parent);
|
||
print_members (parent);
|
||
}
|
||
else
|
||
{
|
||
print_parents (parent);
|
||
print_line (parent);
|
||
print_children (parent);
|
||
}
|
||
|
||
if (bsd_style_output)
|
||
printf ("\n");
|
||
|
||
printf ("-----------------------------------------------\n");
|
||
|
||
if (bsd_style_output)
|
||
printf ("\n");
|
||
}
|
||
|
||
free (timesortsym);
|
||
|
||
if (print_descriptions && !bsd_style_output)
|
||
fsf_callg_blurb (stdout);
|
||
}
|
||
|
||
|
||
static int
|
||
cmp_name (const PTR left, const PTR right)
|
||
{
|
||
const Sym **npp1 = (const Sym **) left;
|
||
const Sym **npp2 = (const Sym **) right;
|
||
|
||
return strcmp ((*npp1)->name, (*npp2)->name);
|
||
}
|
||
|
||
|
||
void
|
||
cg_print_index (void)
|
||
{
|
||
unsigned int sym_index;
|
||
unsigned int nnames, todo, i, j;
|
||
int col, starting_col;
|
||
Sym **name_sorted_syms, *sym;
|
||
const char *filename;
|
||
char buf[20];
|
||
int column_width = (output_width - 1) / 3; /* Don't write in last col! */
|
||
|
||
/* Now, sort regular function name
|
||
alphabetically to create an index. */
|
||
name_sorted_syms = (Sym **) xmalloc ((symtab.len + num_cycles) * sizeof (Sym *));
|
||
|
||
for (sym_index = 0, nnames = 0; sym_index < symtab.len; sym_index++)
|
||
{
|
||
if (ignore_zeros && symtab.base[sym_index].ncalls == 0
|
||
&& symtab.base[sym_index].hist.time == 0)
|
||
continue;
|
||
|
||
name_sorted_syms[nnames++] = &symtab.base[sym_index];
|
||
}
|
||
|
||
qsort (name_sorted_syms, nnames, sizeof (Sym *), cmp_name);
|
||
|
||
for (sym_index = 1, todo = nnames; sym_index <= num_cycles; sym_index++)
|
||
name_sorted_syms[todo++] = &cycle_header[sym_index];
|
||
|
||
printf ("\f\n");
|
||
printf (_("Index by function name\n\n"));
|
||
sym_index = (todo + 2) / 3;
|
||
|
||
for (i = 0; i < sym_index; i++)
|
||
{
|
||
col = 0;
|
||
starting_col = 0;
|
||
|
||
for (j = i; j < todo; j += sym_index)
|
||
{
|
||
sym = name_sorted_syms[j];
|
||
|
||
if (sym->cg.print_flag)
|
||
sprintf (buf, "[%d]", sym->cg.index);
|
||
else
|
||
sprintf (buf, "(%d)", sym->cg.index);
|
||
|
||
if (j < nnames)
|
||
{
|
||
if (bsd_style_output)
|
||
{
|
||
printf ("%6.6s %-19.19s", buf, sym->name);
|
||
}
|
||
else
|
||
{
|
||
col += strlen (buf);
|
||
|
||
for (; col < starting_col + 5; ++col)
|
||
putchar (' ');
|
||
|
||
printf (" %s ", buf);
|
||
col += print_name_only (sym);
|
||
|
||
if (!line_granularity && sym->is_static && sym->file)
|
||
{
|
||
filename = sym->file->name;
|
||
|
||
if (!print_path)
|
||
{
|
||
filename = strrchr (filename, '/');
|
||
|
||
if (filename)
|
||
++filename;
|
||
else
|
||
filename = sym->file->name;
|
||
}
|
||
|
||
printf (" (%s)", filename);
|
||
col += strlen (filename) + 3;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (bsd_style_output)
|
||
{
|
||
printf ("%6.6s ", buf);
|
||
sprintf (buf, _("<cycle %d>"), sym->cg.cyc.num);
|
||
printf ("%-19.19s", buf);
|
||
}
|
||
else
|
||
{
|
||
col += strlen (buf);
|
||
for (; col < starting_col + 5; ++col)
|
||
putchar (' ');
|
||
printf (" %s ", buf);
|
||
sprintf (buf, _("<cycle %d>"), sym->cg.cyc.num);
|
||
printf ("%s", buf);
|
||
col += strlen (buf);
|
||
}
|
||
}
|
||
|
||
starting_col += column_width;
|
||
}
|
||
|
||
printf ("\n");
|
||
}
|
||
|
||
free (name_sorted_syms);
|
||
}
|
||
|
||
/* Compare two arcs based on their usage counts.
|
||
We want to sort in descending order. */
|
||
|
||
static int
|
||
cmp_arc_count (const PTR left, const PTR right)
|
||
{
|
||
const Arc **npp1 = (const Arc **) left;
|
||
const Arc **npp2 = (const Arc **) right;
|
||
|
||
if ((*npp1)->count > (*npp2)->count)
|
||
return -1;
|
||
else if ((*npp1)->count < (*npp2)->count)
|
||
return 1;
|
||
else
|
||
return 0;
|
||
}
|
||
|
||
/* Compare two funtions based on their usage counts.
|
||
We want to sort in descending order. */
|
||
|
||
static int
|
||
cmp_fun_nuses (const PTR left, const PTR right)
|
||
{
|
||
const Sym **npp1 = (const Sym **) left;
|
||
const Sym **npp2 = (const Sym **) right;
|
||
|
||
if ((*npp1)->nuses > (*npp2)->nuses)
|
||
return -1;
|
||
else if ((*npp1)->nuses < (*npp2)->nuses)
|
||
return 1;
|
||
else
|
||
return 0;
|
||
}
|
||
|
||
/* Print a suggested function ordering based on the profiling data.
|
||
|
||
We perform 4 major steps when ordering functions:
|
||
|
||
* Group unused functions together and place them at the
|
||
end of the function order.
|
||
|
||
* Search the highest use arcs (those which account for 90% of
|
||
the total arc count) for functions which have several parents.
|
||
|
||
Group those with the most call sites together (currently the
|
||
top 1.25% which have at least five different call sites).
|
||
|
||
These are emitted at the start of the function order.
|
||
|
||
* Use a greedy placement algorithm to place functions which
|
||
occur in the top 99% of the arcs in the profile. Some provisions
|
||
are made to handle high usage arcs where the parent and/or
|
||
child has already been placed.
|
||
|
||
* Run the same greedy placement algorithm on the remaining
|
||
arcs to place the leftover functions.
|
||
|
||
|
||
The various "magic numbers" should (one day) be tuneable by command
|
||
line options. They were arrived at by benchmarking a few applications
|
||
with various values to see which values produced better overall function
|
||
orderings.
|
||
|
||
Of course, profiling errors, machine limitations (PA long calls), and
|
||
poor cutoff values for the placement algorithm may limit the usefullness
|
||
of the resulting function order. Improvements would be greatly appreciated.
|
||
|
||
Suggestions:
|
||
|
||
* Place the functions with many callers near the middle of the
|
||
list to reduce long calls.
|
||
|
||
* Propagate arc usage changes as functions are placed. Ie if
|
||
func1 and func2 are placed together, arcs to/from those arcs
|
||
to the same parent/child should be combined, then resort the
|
||
arcs to choose the next one.
|
||
|
||
* Implement some global positioning algorithm to place the
|
||
chains made by the greedy local positioning algorithm. Probably
|
||
by examining arcs which haven't been placed yet to tie two
|
||
chains together.
|
||
|
||
* Take a function's size and time into account in the algorithm;
|
||
size in particular is important on the PA (long calls). Placing
|
||
many small functions onto their own page may be wise.
|
||
|
||
* Use better profiling information; many published algorithms
|
||
are based on call sequences through time, rather than just
|
||
arc counts.
|
||
|
||
* Prodecure cloning could improve performance when a small number
|
||
of arcs account for most of the calls to a particular function.
|
||
|
||
* Use relocation information to avoid moving unused functions
|
||
completely out of the code stream; this would avoid severe lossage
|
||
when the profile data bears little resemblance to actual runs.
|
||
|
||
* Propagation of arc usages should also improve .o link line
|
||
ordering which shares the same arc placement algorithm with
|
||
the function ordering code (in fact it is a degenerate case
|
||
of function ordering). */
|
||
|
||
void
|
||
cg_print_function_ordering (void)
|
||
{
|
||
unsigned long sym_index;
|
||
unsigned long arc_index;
|
||
unsigned long used, unused, scratch_index;
|
||
unsigned long unplaced_arc_count, high_arc_count, scratch_arc_count;
|
||
#ifdef __GNUC__
|
||
unsigned long long total_arcs, tmp_arcs_count;
|
||
#else
|
||
unsigned long total_arcs, tmp_arcs_count;
|
||
#endif
|
||
Sym **unused_syms, **used_syms, **scratch_syms;
|
||
Arc **unplaced_arcs, **high_arcs, **scratch_arcs;
|
||
|
||
sym_index = 0;
|
||
used = 0;
|
||
unused = 0;
|
||
scratch_index = 0;
|
||
unplaced_arc_count = 0;
|
||
high_arc_count = 0;
|
||
scratch_arc_count = 0;
|
||
|
||
/* First group all the unused functions together. */
|
||
unused_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
used_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
scratch_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
high_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
scratch_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
unplaced_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
|
||
/* Walk through all the functions; mark those which are never
|
||
called as placed (we'll emit them as a group later). */
|
||
for (sym_index = 0, used = 0, unused = 0; sym_index < symtab.len; sym_index++)
|
||
{
|
||
if (symtab.base[sym_index].ncalls == 0)
|
||
{
|
||
unused_syms[unused++] = &symtab.base[sym_index];
|
||
symtab.base[sym_index].has_been_placed = 1;
|
||
}
|
||
else
|
||
{
|
||
used_syms[used++] = &symtab.base[sym_index];
|
||
symtab.base[sym_index].has_been_placed = 0;
|
||
symtab.base[sym_index].next = 0;
|
||
symtab.base[sym_index].prev = 0;
|
||
symtab.base[sym_index].nuses = 0;
|
||
}
|
||
}
|
||
|
||
/* Sort the arcs from most used to least used. */
|
||
qsort (arcs, numarcs, sizeof (Arc *), cmp_arc_count);
|
||
|
||
/* Compute the total arc count. Also mark arcs as unplaced.
|
||
|
||
Note we don't compensate for overflow if that happens!
|
||
Overflow is much less likely when this file is compiled
|
||
with GCC as it can double-wide integers via long long. */
|
||
total_arcs = 0;
|
||
for (arc_index = 0; arc_index < numarcs; arc_index++)
|
||
{
|
||
total_arcs += arcs[arc_index]->count;
|
||
arcs[arc_index]->has_been_placed = 0;
|
||
}
|
||
|
||
/* We want to pull out those functions which are referenced
|
||
by many highly used arcs and emit them as a group. This
|
||
could probably use some tuning. */
|
||
tmp_arcs_count = 0;
|
||
for (arc_index = 0; arc_index < numarcs; arc_index++)
|
||
{
|
||
tmp_arcs_count += arcs[arc_index]->count;
|
||
|
||
/* Count how many times each parent and child are used up
|
||
to our threshhold of arcs (90%). */
|
||
if ((double)tmp_arcs_count / (double)total_arcs > 0.90)
|
||
break;
|
||
|
||
arcs[arc_index]->child->nuses++;
|
||
}
|
||
|
||
/* Now sort a temporary symbol table based on the number of
|
||
times each function was used in the highest used arcs. */
|
||
memcpy (scratch_syms, used_syms, used * sizeof (Sym *));
|
||
qsort (scratch_syms, used, sizeof (Sym *), cmp_fun_nuses);
|
||
|
||
/* Now pick out those symbols we're going to emit as
|
||
a group. We take up to 1.25% of the used symbols. */
|
||
for (sym_index = 0; sym_index < used / 80; sym_index++)
|
||
{
|
||
Sym *sym = scratch_syms[sym_index];
|
||
Arc *arc;
|
||
|
||
/* If we hit symbols that aren't used from many call sites,
|
||
then we can quit. We choose five as the low limit for
|
||
no particular reason. */
|
||
if (sym->nuses == 5)
|
||
break;
|
||
|
||
/* We're going to need the arcs between these functions.
|
||
Unfortunately, we don't know all these functions
|
||
until we're done. So we keep track of all the arcs
|
||
to the functions we care about, then prune out those
|
||
which are uninteresting.
|
||
|
||
An interesting variation would be to quit when we found
|
||
multi-call site functions which account for some percentage
|
||
of the arcs. */
|
||
arc = sym->cg.children;
|
||
|
||
while (arc)
|
||
{
|
||
if (arc->parent != arc->child)
|
||
scratch_arcs[scratch_arc_count++] = arc;
|
||
arc->has_been_placed = 1;
|
||
arc = arc->next_child;
|
||
}
|
||
|
||
arc = sym->cg.parents;
|
||
|
||
while (arc)
|
||
{
|
||
if (arc->parent != arc->child)
|
||
scratch_arcs[scratch_arc_count++] = arc;
|
||
arc->has_been_placed = 1;
|
||
arc = arc->next_parent;
|
||
}
|
||
|
||
/* Keep track of how many symbols we're going to place. */
|
||
scratch_index = sym_index;
|
||
|
||
/* A lie, but it makes identifying
|
||
these functions easier later. */
|
||
sym->has_been_placed = 1;
|
||
}
|
||
|
||
/* Now walk through the temporary arcs and copy
|
||
those we care about into the high arcs array. */
|
||
for (arc_index = 0; arc_index < scratch_arc_count; arc_index++)
|
||
{
|
||
Arc *arc = scratch_arcs[arc_index];
|
||
|
||
/* If this arc refers to highly used functions, then
|
||
then we want to keep it. */
|
||
if (arc->child->has_been_placed
|
||
&& arc->parent->has_been_placed)
|
||
{
|
||
high_arcs[high_arc_count++] = scratch_arcs[arc_index];
|
||
|
||
/* We need to turn of has_been_placed since we're going to
|
||
use the main arc placement algorithm on these arcs. */
|
||
arc->child->has_been_placed = 0;
|
||
arc->parent->has_been_placed = 0;
|
||
}
|
||
}
|
||
|
||
/* Dump the multi-site high usage functions which are not
|
||
going to be ordered by the main ordering algorithm. */
|
||
for (sym_index = 0; sym_index < scratch_index; sym_index++)
|
||
{
|
||
if (scratch_syms[sym_index]->has_been_placed)
|
||
printf ("%s\n", scratch_syms[sym_index]->name);
|
||
}
|
||
|
||
/* Now we can order the multi-site high use
|
||
functions based on the arcs between them. */
|
||
qsort (high_arcs, high_arc_count, sizeof (Arc *), cmp_arc_count);
|
||
order_and_dump_functions_by_arcs (high_arcs, high_arc_count, 1,
|
||
unplaced_arcs, &unplaced_arc_count);
|
||
|
||
/* Order and dump the high use functions left,
|
||
these typically have only a few call sites. */
|
||
order_and_dump_functions_by_arcs (arcs, numarcs, 0,
|
||
unplaced_arcs, &unplaced_arc_count);
|
||
|
||
/* Now place the rarely used functions. */
|
||
order_and_dump_functions_by_arcs (unplaced_arcs, unplaced_arc_count, 1,
|
||
scratch_arcs, &scratch_arc_count);
|
||
|
||
/* Output any functions not emitted by the order_and_dump calls. */
|
||
for (sym_index = 0; sym_index < used; sym_index++)
|
||
if (used_syms[sym_index]->has_been_placed == 0)
|
||
printf("%s\n", used_syms[sym_index]->name);
|
||
|
||
/* Output the unused functions. */
|
||
for (sym_index = 0; sym_index < unused; sym_index++)
|
||
printf("%s\n", unused_syms[sym_index]->name);
|
||
|
||
unused_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
used_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
scratch_syms = (Sym **) xmalloc (symtab.len * sizeof (Sym *));
|
||
high_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
scratch_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
unplaced_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
|
||
free (unused_syms);
|
||
free (used_syms);
|
||
free (scratch_syms);
|
||
free (high_arcs);
|
||
free (scratch_arcs);
|
||
free (unplaced_arcs);
|
||
}
|
||
|
||
/* Place functions based on the arcs in THE_ARCS with ARC_COUNT entries;
|
||
place unused arcs into UNPLACED_ARCS/UNPLACED_ARC_COUNT.
|
||
|
||
If ALL is nonzero, then place all functions referenced by THE_ARCS,
|
||
else only place those referenced in the top 99% of the arcs in THE_ARCS. */
|
||
|
||
#define MOST 0.99
|
||
static void
|
||
order_and_dump_functions_by_arcs (Arc **the_arcs, unsigned long arc_count,
|
||
int all, Arc **unplaced_arcs,
|
||
unsigned long *unplaced_arc_count)
|
||
{
|
||
#ifdef __GNUC__
|
||
unsigned long long tmp_arcs, total_arcs;
|
||
#else
|
||
unsigned long tmp_arcs, total_arcs;
|
||
#endif
|
||
unsigned int arc_index;
|
||
|
||
/* If needed, compute the total arc count.
|
||
|
||
Note we don't compensate for overflow if that happens! */
|
||
if (! all)
|
||
{
|
||
total_arcs = 0;
|
||
for (arc_index = 0; arc_index < arc_count; arc_index++)
|
||
total_arcs += the_arcs[arc_index]->count;
|
||
}
|
||
else
|
||
total_arcs = 0;
|
||
|
||
tmp_arcs = 0;
|
||
|
||
for (arc_index = 0; arc_index < arc_count; arc_index++)
|
||
{
|
||
Sym *sym1, *sym2;
|
||
Sym *child, *parent;
|
||
|
||
tmp_arcs += the_arcs[arc_index]->count;
|
||
|
||
/* Ignore this arc if it's already been placed. */
|
||
if (the_arcs[arc_index]->has_been_placed)
|
||
continue;
|
||
|
||
child = the_arcs[arc_index]->child;
|
||
parent = the_arcs[arc_index]->parent;
|
||
|
||
/* If we're not using all arcs, and this is a rarely used
|
||
arc, then put it on the unplaced_arc list. Similarly
|
||
if both the parent and child of this arc have been placed. */
|
||
if ((! all && (double)tmp_arcs / (double)total_arcs > MOST)
|
||
|| child->has_been_placed || parent->has_been_placed)
|
||
{
|
||
unplaced_arcs[(*unplaced_arc_count)++] = the_arcs[arc_index];
|
||
continue;
|
||
}
|
||
|
||
/* If all slots in the parent and child are full, then there isn't
|
||
anything we can do right now. We'll place this arc on the
|
||
unplaced arc list in the hope that a global positioning
|
||
algorithm can use it to place function chains. */
|
||
if (parent->next && parent->prev && child->next && child->prev)
|
||
{
|
||
unplaced_arcs[(*unplaced_arc_count)++] = the_arcs[arc_index];
|
||
continue;
|
||
}
|
||
|
||
/* If the parent is unattached, then find the closest
|
||
place to attach it onto child's chain. Similarly
|
||
for the opposite case. */
|
||
if (!parent->next && !parent->prev)
|
||
{
|
||
int next_count = 0;
|
||
int prev_count = 0;
|
||
Sym *prev = child;
|
||
Sym *next = child;
|
||
|
||
/* Walk to the beginning and end of the child's chain. */
|
||
while (next->next)
|
||
{
|
||
next = next->next;
|
||
next_count++;
|
||
}
|
||
|
||
while (prev->prev)
|
||
{
|
||
prev = prev->prev;
|
||
prev_count++;
|
||
}
|
||
|
||
/* Choose the closest. */
|
||
child = next_count < prev_count ? next : prev;
|
||
}
|
||
else if (! child->next && !child->prev)
|
||
{
|
||
int next_count = 0;
|
||
int prev_count = 0;
|
||
Sym *prev = parent;
|
||
Sym *next = parent;
|
||
|
||
while (next->next)
|
||
{
|
||
next = next->next;
|
||
next_count++;
|
||
}
|
||
|
||
while (prev->prev)
|
||
{
|
||
prev = prev->prev;
|
||
prev_count++;
|
||
}
|
||
|
||
parent = prev_count < next_count ? prev : next;
|
||
}
|
||
else
|
||
{
|
||
/* Couldn't find anywhere to attach the functions,
|
||
put the arc on the unplaced arc list. */
|
||
unplaced_arcs[(*unplaced_arc_count)++] = the_arcs[arc_index];
|
||
continue;
|
||
}
|
||
|
||
/* Make sure we don't tie two ends together. */
|
||
sym1 = parent;
|
||
if (sym1->next)
|
||
while (sym1->next)
|
||
sym1 = sym1->next;
|
||
else
|
||
while (sym1->prev)
|
||
sym1 = sym1->prev;
|
||
|
||
sym2 = child;
|
||
if (sym2->next)
|
||
while (sym2->next)
|
||
sym2 = sym2->next;
|
||
else
|
||
while (sym2->prev)
|
||
sym2 = sym2->prev;
|
||
|
||
if (sym1 == child
|
||
&& sym2 == parent)
|
||
{
|
||
/* This would tie two ends together. */
|
||
unplaced_arcs[(*unplaced_arc_count)++] = the_arcs[arc_index];
|
||
continue;
|
||
}
|
||
|
||
if (parent->next)
|
||
{
|
||
/* Must attach to the parent's prev field. */
|
||
if (! child->next)
|
||
{
|
||
/* parent-prev and child-next */
|
||
parent->prev = child;
|
||
child->next = parent;
|
||
the_arcs[arc_index]->has_been_placed = 1;
|
||
}
|
||
}
|
||
else if (parent->prev)
|
||
{
|
||
/* Must attach to the parent's next field. */
|
||
if (! child->prev)
|
||
{
|
||
/* parent-next and child-prev */
|
||
parent->next = child;
|
||
child->prev = parent;
|
||
the_arcs[arc_index]->has_been_placed = 1;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* Can attach to either field in the parent, depends
|
||
on where we've got space in the child. */
|
||
if (child->prev)
|
||
{
|
||
/* parent-prev and child-next. */
|
||
parent->prev = child;
|
||
child->next = parent;
|
||
the_arcs[arc_index]->has_been_placed = 1;
|
||
}
|
||
else
|
||
{
|
||
/* parent-next and child-prev. */
|
||
parent->next = child;
|
||
child->prev = parent;
|
||
the_arcs[arc_index]->has_been_placed = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Dump the chains of functions we've made. */
|
||
for (arc_index = 0; arc_index < arc_count; arc_index++)
|
||
{
|
||
Sym *sym;
|
||
if (the_arcs[arc_index]->parent->has_been_placed
|
||
|| the_arcs[arc_index]->child->has_been_placed)
|
||
continue;
|
||
|
||
sym = the_arcs[arc_index]->parent;
|
||
|
||
/* If this symbol isn't attached to any other
|
||
symbols, then we've got a rarely used arc.
|
||
|
||
Skip it for now, we'll deal with them later. */
|
||
if (sym->next == NULL
|
||
&& sym->prev == NULL)
|
||
continue;
|
||
|
||
/* Get to the start of this chain. */
|
||
while (sym->prev)
|
||
sym = sym->prev;
|
||
|
||
while (sym)
|
||
{
|
||
/* Mark it as placed. */
|
||
sym->has_been_placed = 1;
|
||
printf ("%s\n", sym->name);
|
||
sym = sym->next;
|
||
}
|
||
}
|
||
|
||
/* If we want to place all the arcs, then output
|
||
those which weren't placed by the main algorithm. */
|
||
if (all)
|
||
for (arc_index = 0; arc_index < arc_count; arc_index++)
|
||
{
|
||
Sym *sym;
|
||
if (the_arcs[arc_index]->parent->has_been_placed
|
||
|| the_arcs[arc_index]->child->has_been_placed)
|
||
continue;
|
||
|
||
sym = the_arcs[arc_index]->parent;
|
||
|
||
sym->has_been_placed = 1;
|
||
printf ("%s\n", sym->name);
|
||
}
|
||
}
|
||
|
||
/* Compare two function_map structs based on file name.
|
||
We want to sort in ascending order. */
|
||
|
||
static int
|
||
cmp_symbol_map (const void * l, const void * r)
|
||
{
|
||
return filename_cmp (((struct function_map *) l)->file_name,
|
||
((struct function_map *) r)->file_name);
|
||
}
|
||
|
||
/* Print a suggested .o ordering for files on a link line based
|
||
on profiling information. This uses the function placement
|
||
code for the bulk of its work. */
|
||
|
||
void
|
||
cg_print_file_ordering (void)
|
||
{
|
||
unsigned long scratch_arc_count;
|
||
unsigned long arc_index;
|
||
unsigned long sym_index;
|
||
Arc **scratch_arcs;
|
||
char *last;
|
||
|
||
scratch_arc_count = 0;
|
||
|
||
scratch_arcs = (Arc **) xmalloc (numarcs * sizeof (Arc *));
|
||
for (arc_index = 0; arc_index < numarcs; arc_index++)
|
||
{
|
||
if (! arcs[arc_index]->parent->mapped
|
||
|| ! arcs[arc_index]->child->mapped)
|
||
arcs[arc_index]->has_been_placed = 1;
|
||
}
|
||
|
||
order_and_dump_functions_by_arcs (arcs, numarcs, 0,
|
||
scratch_arcs, &scratch_arc_count);
|
||
|
||
/* Output .o's not handled by the main placement algorithm. */
|
||
for (sym_index = 0; sym_index < symtab.len; sym_index++)
|
||
{
|
||
if (symtab.base[sym_index].mapped
|
||
&& ! symtab.base[sym_index].has_been_placed)
|
||
printf ("%s\n", symtab.base[sym_index].name);
|
||
}
|
||
|
||
qsort (symbol_map, symbol_map_count, sizeof (struct function_map), cmp_symbol_map);
|
||
|
||
/* Now output any .o's that didn't have any text symbols. */
|
||
last = NULL;
|
||
for (sym_index = 0; sym_index < symbol_map_count; sym_index++)
|
||
{
|
||
unsigned int index2;
|
||
|
||
/* Don't bother searching if this symbol
|
||
is the same as the previous one. */
|
||
if (last && !filename_cmp (last, symbol_map[sym_index].file_name))
|
||
continue;
|
||
|
||
for (index2 = 0; index2 < symtab.len; index2++)
|
||
{
|
||
if (! symtab.base[index2].mapped)
|
||
continue;
|
||
|
||
if (!filename_cmp (symtab.base[index2].name,
|
||
symbol_map[sym_index].file_name))
|
||
break;
|
||
}
|
||
|
||
/* If we didn't find it in the symbol table, then it must
|
||
be a .o with no text symbols. Output it last. */
|
||
if (index2 == symtab.len)
|
||
printf ("%s\n", symbol_map[sym_index].file_name);
|
||
last = symbol_map[sym_index].file_name;
|
||
}
|
||
}
|