00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015 """
00016 PyMite Heap Dump
00017 ================
00018
00019 Parses a heap dump into human-readable format.
00020
00021 The heap dump file is created by inserting a calls to heap_dump()
00022 inside heap_gcRun(). Using two calls, a before and after, is ideal.
00023
00024 The dump format is:
00025
00026 =========== ==========================================
00027 NumBytes Contents
00028 =========== ==========================================
00029 6 string: PMDUMP or PMUDMP depending on target endianess (little and big respectively)
00030 2 uint16: pointer size
00031 2 uint16: dump format version
00032 2 uint16: bifield of pmfeatures enabled on target
00033 4 uint32: HEAP_SIZE
00034 p pointer to heap start
00035 HEAP_SIZE contents of heap (byte array)
00036 4 uint16: NUM_ROOTS
00037 NUM_ROOTS*p pointers to root objects
00038 =========== ==========================================
00039
00040 The heap_dump() function names files incrementally starting from:
00041
00042 pmheapdump00.bin
00043 pmheapdump01.bin
00044 ...
00045 pmheapdumpNN.bin
00046 """
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057 import os, struct, sys, types, UserDict, collections
00058
00059 try:
00060 import cStringIO as StringIO
00061 except:
00062 import StringIO
00063
00064
00065 def _ellipse(string, length):
00066 """Truncates a string to a given size with ellipses
00067 """
00068 if len(string) < length:
00069 return string
00070 else:
00071 return string[0 : length-3] + '...'
00072
00073
00074 def _dot_escape(string):
00075 return string.replace('<', '<').replace('>', '>')
00076
00077
00078 def unpack_fp(fmt, fp, increment=True):
00079 """Unpacks a structure from a file, increment the position if set.
00080 """
00081
00082 if increment:
00083 return struct.unpack(fmt, fp.read(struct.calcsize(fmt)))
00084 else:
00085 pos = fp.tell()
00086 ret = struct.unpack(fmt, fp.read(struct.calcsize(fmt)))
00087 fp.seek(pos)
00088 return ret
00089
00090
00091 PmFieldInfo = collections.namedtuple('PmFieldInfo', 'name type mul')
00092 PmBitfieldInfo = collections.namedtuple('PmFieldInfo', 'type fields mul')
00093
00094
00095 class PmTypeInfo(object):
00096 """Model of an object type
00097 """
00098
00099 def __init__(self, name, fmt):
00100 """Initializes a new object type from name and a format string
00101 The format consist of a list of field name, type and optionnal multiplicity
00102 fmt = "fieldname1:fieldtype1[:multiplicity1],fieldname2:fieldtype2[:multiplicity2],..."
00103 where:
00104 fieldname : a string naming a field...
00105 fieldtype : a type understand by python struct [c, b, B, ?, h, H, i, I, l, L, q, Q, f, d, s, p, P] or '.' for a bit
00106 P (pointer) is translated to the correct size based on the dump header information
00107 multiplicity : number of item for a list. multiplicity can be:
00108 any positive integer
00109 a string naming a field already described
00110 Multiplicity can also specify:
00111 '*' to read as much as possible in the limit of the size object
00112 '<name' to read as long as memory offset is lower than then value of field name
00113 """
00114
00115 self._parse_fmt(fmt)
00116 self.name = name
00117
00118
00119 def _parse_fmt(self, fmt):
00120
00121 self.fields = []
00122 for f in map(lambda s: s.strip(), fmt.split(',')):
00123 if f == '':
00124 continue
00125 name, typ = f.split(':', 1)
00126 if ':' in typ:
00127 typ, mul = typ.split(':')
00128 try:
00129 mul = int(mul)
00130 except:
00131 pass
00132 else:
00133 mul = False
00134
00135 self.fields.append(PmFieldInfo(name, typ, mul))
00136
00137
00138 bitfield = None
00139 self._rawfields = []
00140
00141 for i, f in enumerate(self.fields):
00142
00143 if f.type != '.':
00144 self._rawfields.append(f)
00145 continue
00146
00147 if bitfield == None:
00148 bitfield = []
00149
00150 bitfield.append(f)
00151
00152
00153 if i + 1 < len(self.fields) and self.fields[i + 1].type == '.':
00154 continue
00155
00156
00157 bitcount = sum([sf.mul for sf in bitfield])
00158 bytes = 1
00159 while (bytes * 8 < bitcount):
00160 bytes *= bytes
00161 typechr = [None, 'B', 'H', None, 'I', None, None, None, 'Q'][bytes]
00162
00163 self._rawfields.append(PmBitfieldInfo(typechr, bitfield, False))
00164 bitfield = None
00165
00166
00167 def _calc_fieldmap(self, obj):
00168 """Computes fieldmap and format
00169 Returns a triple(format, : a struct format string
00170 fielmap, : a list of Pm(Bit)FieldInfo
00171 remaining : number of field that cannot ne computed due to a missing value
00172 )
00173 """
00174
00175 d = obj.data
00176 fmt = obj.heap.endianchr + "H"
00177 fieldmap = [None]
00178
00179 for i, f in enumerate(self._rawfields):
00180 typechr = (f.type == 'P') and obj.heap.ptrchr or f.type
00181
00182 while (struct.calcsize(fmt) % struct.calcsize(typechr)!=0):
00183 fmt += 'x'
00184
00185 if f.mul == False:
00186 fmt += typechr
00187 fieldmap.append(f)
00188
00189 else:
00190 d[f.name] = []
00191 if isinstance(f.mul, int):
00192 fmt += typechr * f.mul
00193 fieldmap += [f] * f.mul
00194 elif f.mul == '*':
00195 while struct.calcsize(fmt)<obj.size:
00196 fmt += typechr
00197 fieldmap.append(f)
00198 elif f.mul.startswith('<'):
00199 if f.mul[1:] not in d:
00200 return (fmt, fieldmap, len(self._rawfields)-i)
00201
00202 while struct.calcsize(fmt + typechr) + obj.addr \
00203 + obj.heap.base < d[f.mul[1:]]:
00204
00205 fmt += typechr
00206 fieldmap += [f]
00207 else:
00208 if f.mul not in d:
00209 return (fmt, fieldmap, len(self._rawfields)-i)
00210
00211 fmt += typechr * d[f.mul]
00212 fieldmap += [f] * d[f.mul]
00213
00214 return (fmt, fieldmap, 0)
00215
00216
00217 def PmObjectClass(dumpversion, features):
00218
00219 class PmObject(UserDict.UserDict):
00220 """A model of an object.
00221 """
00222
00223 PM_TYPES = (
00224 PmTypeInfo('NON', ""),
00225 PmTypeInfo("INT", "val:i"),
00226 PmTypeInfo("FLT", "val:f"),
00227 PmTypeInfo("STR", "len:H,"+
00228 (features.USE_STRING_CACHE and "cache_next:P," or "") +
00229 "val:B:len"),
00230 PmTypeInfo("TUP", "len:H,items:P:len"),
00231 PmTypeInfo("COB", "codeimg:P,names:P,consts:P,code:P"),
00232 PmTypeInfo("MOD", "co:P,attrs:P,globals:P," +
00233 (features.HAVE_DEFAULTARGS and "defaultargs:P," or "") +
00234 (features.HAVE_CLOSURES and "closure:P," or "")),
00235 PmTypeInfo("CLO", "attrs:P,bases:P"),
00236 PmTypeInfo("FXN", "co:P,attrs:P,globals:P," +
00237 (features.HAVE_DEFAULTARGS and "defaultargs:P," or "") +
00238 (features.HAVE_CLOSURES and "closure:P" or "")),
00239 PmTypeInfo("CLI", "class:P,attrs:P"),
00240 PmTypeInfo("CIM", "data:B:*"),
00241 PmTypeInfo("NIM", ""),
00242 PmTypeInfo("NOB", "argcount:B,funcidx:H"),
00243 PmTypeInfo("THR", "frame:P,interpctrl:I"),
00244 PmTypeInfo("x", ""),
00245 PmTypeInfo("BOL", "val:i"),
00246 PmTypeInfo("CIO", "data:B:*"),
00247 PmTypeInfo("MTH", "instance:P,func:P,attrs:P"),
00248 PmTypeInfo("LST", "len:H,sgl:P"),
00249 PmTypeInfo("DIC", "len:H,keys:P,vals:P"),
00250 PmTypeInfo("x", ""),
00251 PmTypeInfo("x", ""),
00252 PmTypeInfo("x", ""),
00253 PmTypeInfo("x", ""),
00254 PmTypeInfo("x", ""),
00255 PmTypeInfo("FRM", "back:P,func:P,memspace:B,ip:P,blockstack:P,"
00256 "attrs:P,globals:P,sp:P,isImport:.," +
00257 (features.HAVE_CLASSES and "isInit:.," or "") +
00258 "locals:P:<sp"),
00259 PmTypeInfo("BLK", "sp:P,handler:P,type:B,next:P"),
00260 PmTypeInfo("SEG", "items:P:8,next:P"),
00261 PmTypeInfo("SGL", "rootseg:P,lastseg:P,length:H"),
00262 PmTypeInfo("SQI", "sequence:P,index:H"),
00263 PmTypeInfo("NFM", "back:P,func:P,stack:P,active:B,numlocals:B,"
00264 "locals:P:8"),
00265 )
00266
00267 FREE_TYPE = PmTypeInfo("FRE", "prev:P,next:P")
00268
00269
00270 def __init__(self, heap):
00271 """Initializes the object at the current file location
00272 """
00273 UserDict.UserDict.__init__(self)
00274
00275 self.is_dotted = False
00276 self.is_dotrev = False
00277
00278 self.heap = heap
00279 self.fp = fp = self.heap.rawheap
00280 self.addr = self.fp.tell()
00281
00282 od = unpack_fp(heap.endianchr + "H", fp, False)[0]
00283 self.mark = (' ','M')[(od & 0x4000) == 0x4000]
00284 self.free = (' ','F')[(od & 0x8000) == 0x8000]
00285
00286 if self.free == 'F':
00287 self.size = (od & 0x3FFF) << 2
00288 self.objtype = self.FREE_TYPE
00289
00290 else:
00291 self.size = (od & 0x01FF) << 2
00292 assert self.size > 0
00293 self.typeindex = (od >> 9) & 0x1f
00294 self.objtype = PmObject.PM_TYPES[self.typeindex]
00295 if self.objtype.name == 'x':
00296 raise Exception("unknown object type", self.typeindex)
00297
00298 self.type = self.objtype.name.lower()
00299
00300 self.parse()
00301
00302 self.fp.seek(self.addr + self.size)
00303
00304
00305 def parse(self,):
00306 """Parses data at the current file location
00307 """
00308 d = self.data
00309
00310 fmt, fieldmap, remaining = self.objtype._calc_fieldmap(self)
00311
00312 results = unpack_fp(fmt, self.fp, False)
00313
00314
00315 for r, f in zip(results, fieldmap):
00316 if f == None:
00317 continue
00318 elif isinstance(f, PmBitfieldInfo):
00319 for bf in f.fields:
00320 d[bf.name] = r & ((1 << bf.mul) - 1)
00321 r = r >> bf.mul
00322
00323 elif f.mul == False:
00324 d[f.name] = r
00325
00326 elif f.mul == 'lastv':
00327 if r != 0:
00328 d[f.name].append(r)
00329 else:
00330 break
00331
00332 else:
00333 d[f.name].append(r)
00334
00335 if remaining:
00336 if not self.objtype._calc_fieldmap(self)[2] < remaining:
00337 raise Exception("Cannot compute %s field length"
00338 % self.objtype._rawfields[-remaining].name)
00339
00340
00341 self.parse()
00342
00343
00344 def __str__(self,):
00345
00346 d = self.data
00347 result = []
00348 result.append("%s %s %d %s%s: " % (
00349 hex(self.addr+self.heap.base),
00350 self.type,
00351 self.size,
00352 self.mark,
00353 self.free,))
00354
00355 values = []
00356 for f in self.objtype.fields:
00357 typechr = (f.type in ['P', '.']) and "0x%x" or "%d"
00358 if f.mul == False:
00359 values.append(("%s=" + typechr) % (f.name, d[f.name]))
00360 elif self.objtype.name == "STR" and f.name == "val":
00361 values.append("val=%s"
00362 % _ellipse("".join(map(chr, d['val'])), 30))
00363 elif self.objtype.name == 'CIO' and f.name == 'data':
00364 values.append("data=%s"
00365 % _ellipse(repr("".join(map(chr, d['data']))),
00366 30))
00367 else:
00368 values.append(
00369 "%s=[%s]"
00370 % (f.name, ", ".join([typechr % v for v in d[f.name]])))
00371 result.append(", ".join(values))
00372
00373 return "".join(result)
00374
00375
00376 def __repr__(self):
00377 return "<0x%x %s %d>" \
00378 % (self.addr + self.heap.base,
00379 self.objtype.name.lower(),
00380 self.size)
00381
00382
00383 COLOR = ["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
00384 "beige", "bisque", "blanchedalmond", "blue",
00385 "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse",
00386 "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson",
00387 "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray",
00388 "darkgreen", "darkgrey", "darkkhaki", "darkmagenta",
00389 "darkolivegreen", "darkorange", "darkorchid", "darkred",
00390 "darksalmon", "darkseagreen", "darkslateblue", "darkslategray",
00391 "darkslategrey", "darkturquoise", "darkviolet", "deeppink",
00392 "deepskyblue", "dimgray", "dimgrey", "dodgerblue"]
00393
00394
00395 def dotstring(self):
00396 """A DOT representation of the object
00397 """
00398
00399 if self.is_dotted:
00400 return ""
00401
00402 self.is_dotted = True
00403
00404 d = self.data
00405
00406
00407 if self.type == 'dic' and d['len']>0:
00408 seg = self.heap.data[d['vals']].data['rootseg']
00409 while seg:
00410 self.heap.data[seg].is_dotrev = True
00411 seg = self.heap.data[seg].data['next']
00412
00413 result = []
00414
00415 result.append('"0x%x" [style=filled, fillcolor=%s, colorscheme=svg,'
00416 ' label="%s"];'
00417 % (self.addr+self.heap.base,
00418 self.COLOR[getattr(self, 'typeindex', 0)],
00419 self._dot_label()))
00420
00421 if (self.type == "sgl" and d['rootseg'] in self.heap.data
00422 and d['rootseg'] != d['lastseg']):
00423 result.append("{ rank=same;")
00424 seg = d['rootseg']
00425 while seg in self.heap.data:
00426 result.append(self.heap.data[seg].dotstring())
00427 seg = self.heap.data[seg].data['next']
00428 result.append('}')
00429 return "\n".join(result)
00430
00431
00432 def _dot_label(self):
00433 """Label for the dot node
00434 """
00435
00436 d = self.data
00437 label = []
00438 label.append('{')
00439 label.append(_dot_escape(repr(self)))
00440
00441 values = []
00442 for f in self.objtype.fields:
00443 if f.type == 'P': continue
00444 if self.objtype.name == "STR" and f.name == "val" \
00445 and d['val'] != None:
00446 values.append("val=%s"
00447 % _dot_escape(_ellipse("".join(map(chr, d['val'])), 20))
00448 )
00449 elif self.objtype.name == "CIO" and f.name == "data":
00450 values.append("data=%s"
00451 % _dot_escape(_ellipse(repr(d['data']), 20)))
00452 else:
00453 values.append("%s=%s"
00454 % (_dot_escape(f.name), _dot_escape(str(d[f.name]))))
00455 if len(values):
00456 label.append("|{")
00457 label.append("|".join(values))
00458 label.append("}")
00459
00460 pointers = []
00461 for f in self.objtype.fields:
00462 if f.type != 'P':
00463 continue
00464 if f.name == "cache_next" and d[f.name] in self.heap.data:
00465 pointers.append("%s=%s"%(f.name, hex(d[f.name])))
00466 else:
00467 pointers.append("<%s> %s" % (f.name, f.name))
00468 if len(pointers):
00469 label.append('|{')
00470 label.append("|".join(pointers))
00471 label.append('}')
00472 label.append('}')
00473
00474 return "".join(label)
00475
00476
00477 def dotedges(self):
00478 """Edges (pointers) leaving this object
00479 """
00480
00481 d = self.data
00482 result = []
00483
00484 for f in self.objtype.fields:
00485 if f.type != 'P':
00486 continue
00487 if f.name == "cache_next":
00488 continue
00489 if f.mul == False:
00490 if d[f.name] == 0:
00491 continue
00492 result.append(self._dotedge(f.name, d[f.name]))
00493 else:
00494 for i, m in enumerate(d[f.name]):
00495 if m == 0:
00496 continue
00497 result.append(self._dotedge(f.name, m, str(i)))
00498
00499 if self.type == "dic" and d['len'] > 0:
00500 i = 0
00501 sgls = tuple(map(lambda p: self.heap.data[d[p]],
00502 ("keys", "vals")))
00503 segs = tuple(map(lambda l: self.heap.data[l.data['rootseg']],
00504 sgls))
00505 iters = tuple(map(lambda s: iter(s.data['items']), segs))
00506 while i < d['len']:
00507 try:
00508 key, val = tuple(map(next, iters))
00509 result.append('"0x%x" -> "0x%x" '
00510 '[style=dotted, weight=50];'%(key, val))
00511 i += 1
00512 except StopIteration:
00513 segs = tuple(map(
00514 lambda s: self.heap.data[s.data['next']] , segs))
00515 iters = tuple(map(
00516 lambda s: iter(s.data['items']), segs))
00517
00518 return "\n".join(result)
00519
00520
00521 def _dotedge(self, name, value, label = None):
00522
00523 style = []
00524 if label != None:
00525 style.append("label=%s" % label)
00526 if self.is_dotrev:
00527 style.append("dir=back")
00528
00529 style = len(style) and (" [" + ", ".join(style) + "]") or ""
00530
00531 if self.is_dotrev:
00532 return '"0x%x" -> "0x%x":%s%s;' \
00533 % (value, self.addr+self.heap.base, name, style)
00534 else:
00535 return '"0x%x":%s -> "0x%x"%s;' \
00536 % (self.addr+self.heap.base, name, value, style)
00537
00538 return PmObject
00539
00540
00541 class PmHeap(UserDict.UserDict):
00542 """A model of the heap.
00543 """
00544
00545 FEATURES = ['USE_STRING_CACHE', 'HAVE_DEFAULTARGS', 'HAVE_CLOSURES',
00546 'HAVE_CLASSES']
00547
00548
00549 def __init__(self, fp):
00550 """Initializes the heap based on the given dump file.
00551 """
00552 UserDict.UserDict.__init__(self)
00553
00554 self.is_parsed = False
00555
00556 self._sense_fmt(fp)
00557 self.version, features, self.size, self.base = \
00558 unpack_fp(self.endianchr + "2HI" + self.ptrchr, fp)
00559
00560 if self.version != 1:
00561 raise Exception('Dump version %d not supported' % self.version)
00562
00563 self.features = \
00564 type("pmFeatures",
00565 (object,),
00566 dict(zip(self.FEATURES, [False] * len(self.FEATURES))))()
00567 f = 0
00568 while(features):
00569 setattr(self.features,
00570 self.FEATURES[f],
00571 features & 1 and True or False)
00572 f = f + 1
00573 features = features >> 1
00574
00575 self.rawheap = StringIO.StringIO(fp.read(self.size))
00576
00577 num_roots = unpack_fp("I", fp)[0]
00578 roots = {}
00579 (roots['None'],
00580 roots['False'],
00581 roots['True'],
00582 roots['Zero'],
00583 roots['One'],
00584 roots['NegOne'],
00585 roots['CodeStr'],
00586 roots['Builtins'],
00587 roots['NativeFrame'],
00588 roots['ThreadList']) = \
00589 unpack_fp(self.endianchr + (self.ptrchr * num_roots), fp)
00590 self.roots = roots
00591 self.PmObjectClass = PmObjectClass(self.version, self.features)
00592
00593 fp.close()
00594
00595
00596 def _sense_fmt(self, fp):
00597 """Senses pmdump format (endianess, pointer size)
00598 depending on the first 8 bytes
00599 """
00600
00601 magic = fp.read(6)
00602
00603 if magic == "PMDUMP":
00604 self.endianess = "little"
00605 self.endianchr = '<'
00606 elif magic == "PMUDMP":
00607 self.endianess = "big"
00608 self.endianchr = '>'
00609 else:
00610 raise Exception("Not a PMDUMP format")
00611
00612 self.ptrsize = unpack_fp(self.endianchr+"H", fp)[0]
00613 self.ptrchr = [None, 'B', 'H', None, 'I',
00614 None, None, None, 'Q'][self.ptrsize]
00615 if self.ptrchr == None:
00616 raise Exception('invalid pointer size')
00617
00618
00619 def parse_heap(self,):
00620 """Parses the heap into a dict of key=address, value=object items
00621 """
00622 self.rawheap.seek(0)
00623 while self.rawheap.tell() < self.size:
00624 addr = self.rawheap.tell() + self.base
00625 self.data[addr] = self.PmObjectClass(self)
00626 self.is_parsed = True
00627
00628
00629 def __getitem__(self, indx):
00630 """Returns the object at the given address
00631 or the string of bytes defined by the slice.
00632 """
00633
00634 if type(indx) == types.IntType:
00635 if is_parsed:
00636 return self.data[indx]
00637 else:
00638 self.rawheap.seek(indx)
00639 return self.PmObjectClass(self)
00640
00641
00642 elif type(indx) == types.SliceType:
00643 return self.rawheap[indx.start - self.base : indx.stop - self.base]
00644
00645 else:
00646 assert False, "Bad type to heap[%s]" % type(indx)
00647
00648
00649 def __str__(self):
00650
00651 d = self.data
00652
00653 obj = filter(lambda o: o.type != "fre", self.data.values())
00654 free = filter(lambda o: o.type == "fre", self.data.values())
00655
00656 result = []
00657 result.append("dump : version=%d, ptr=%dbytes, %s-endian, features=%s"
00658 % (self.version, self.ptrsize, self.endianess, self.features))
00659 result.append("roots : "
00660 + ", ".join(map(lambda kv: "%s=0x%x" % kv, self.roots.iteritems())))
00661 result.append("heap : size=%d, base=%x" % (self.size, self.base))
00662 result.append("summary : %d bytes in %d objects, %d free bytes" %
00663 (sum([o.size for o in obj]),
00664 len(obj),
00665 sum([o.size for o in free])))
00666
00667 for o in sorted(d.values(), key=lambda o: o.addr):
00668 result.append(str(o))
00669
00670 result.append('')
00671
00672 return "\n".join(result)
00673
00674
00675 def dotstring(self):
00676 """A DOT representation of the heap
00677 """
00678
00679 d = self.data
00680 result = []
00681 result.append("digraph pmheapdump {")
00682
00683
00684 result.append('{ rank=same;')
00685 for r, m in self.roots.iteritems():
00686 result.append("%s;" % r)
00687 result.append('}')
00688
00689 result.append("{ node [shape=record];")
00690 for o in sorted(d.values(), key=lambda o: o.addr):
00691 result.append(o.dotstring())
00692 result.append("}")
00693
00694 for r, m in self.roots.iteritems():
00695 result.append('%s -> "0x%x";' % (r, m))
00696
00697 for o in sorted(d.values(), key=lambda o: o.addr):
00698 result.append(o.dotedges())
00699
00700 result.append("}")
00701
00702 result.append('')
00703
00704 return "\n".join(filter(len, result))
00705
00706
00707 def main():
00708 from optparse import OptionParser
00709
00710 parser = OptionParser(usage="usage: %prog [options] [dumpfile [output]]")
00711 parser.add_option("-f", "--format",
00712 dest="format", default='list', choices=['list', 'dot'],
00713 help="output format: list or dot [default: %default]")
00714
00715 (options, args) = parser.parse_args()
00716
00717 if len(args) == 0:
00718 fp = open(os.path.join(os.path.curdir, "pmheapdump00.bin"), 'rb')
00719 out = sys.stdout
00720 elif len(args) == 1:
00721 fp = open(args[0], 'rb')
00722 out = sys.stdout
00723 elif len(args) == 2:
00724 fp = open(args[0], 'rb')
00725 out = open(args[1], 'w')
00726 else:
00727 print "too many arguments"
00728 parser.print_help()
00729 sys.exit()
00730
00731 heap0 = PmHeap(fp)
00732 heap0.parse_heap()
00733
00734 if options.format == 'list':
00735 out.write(str(heap0))
00736 elif options.format == 'dot':
00737 out.write(heap0.dotstring())
00738
00739
00740 if __name__ == "__main__":
00741 main()