| OLD | NEW |
| 1 # Copyright 2017 The Chromium Authors. All rights reserved. | 1 # Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 """Classes that comprise the data model for binary size analysis. | 4 """Classes that comprise the data model for binary size analysis. |
| 5 | 5 |
| 6 The primary classes are Symbol, and SymbolGroup. | 6 The primary classes are Symbol, and SymbolGroup. |
| 7 | 7 |
| 8 Description of common properties: | 8 Description of common properties: |
| 9 * address: The start address of the symbol. | 9 * address: The start address of the symbol. |
| 10 May be 0 (e.g. for .bss or for SymbolGroups). | 10 May be 0 (e.g. for .bss or for SymbolGroups). |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 48 'd': '.data', | 48 'd': '.data', |
| 49 'r': '.rodata', | 49 'r': '.rodata', |
| 50 't': '.text', | 50 't': '.text', |
| 51 } | 51 } |
| 52 | 52 |
| 53 FLAG_ANONYMOUS = 1 | 53 FLAG_ANONYMOUS = 1 |
| 54 FLAG_STARTUP = 2 | 54 FLAG_STARTUP = 2 |
| 55 FLAG_UNLIKELY = 4 | 55 FLAG_UNLIKELY = 4 |
| 56 FLAG_REL = 8 | 56 FLAG_REL = 8 |
| 57 FLAG_REL_LOCAL = 16 | 57 FLAG_REL_LOCAL = 16 |
| 58 FLAG_GENERATED_SOURCE = 32 |
| 58 | 59 |
| 59 | 60 |
| 60 class SizeInfo(object): | 61 class SizeInfo(object): |
| 61 """Represents all size information for a single binary. | 62 """Represents all size information for a single binary. |
| 62 | 63 |
| 63 Fields: | 64 Fields: |
| 64 section_sizes: A dict of section_name -> size. | 65 section_sizes: A dict of section_name -> size. |
| 65 symbols: A SymbolGroup containing all symbols, sorted by address. | 66 symbols: A SymbolGroup containing all symbols, sorted by address. |
| 66 metadata: A dict. | 67 metadata: A dict. |
| 67 """ | 68 """ |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 129 | 130 |
| 130 @property | 131 @property |
| 131 def end_address(self): | 132 def end_address(self): |
| 132 return self.address + self.size_without_padding | 133 return self.address + self.size_without_padding |
| 133 | 134 |
| 134 @property | 135 @property |
| 135 def is_anonymous(self): | 136 def is_anonymous(self): |
| 136 return bool(self.flags & FLAG_ANONYMOUS) | 137 return bool(self.flags & FLAG_ANONYMOUS) |
| 137 | 138 |
| 138 @property | 139 @property |
| 140 def generated_source(self): |
| 141 return bool(self.flags & FLAG_GENERATED_SOURCE) |
| 142 |
| 143 @generated_source.setter |
| 144 def generated_source(self, value): |
| 145 if value: |
| 146 self.flags |= FLAG_GENERATED_SOURCE |
| 147 else: |
| 148 self.flags &= ~FLAG_GENERATED_SOURCE |
| 149 |
| 150 @property |
| 139 def num_aliases(self): | 151 def num_aliases(self): |
| 140 return len(self.aliases) if self.aliases else 1 | 152 return len(self.aliases) if self.aliases else 1 |
| 141 | 153 |
| 142 def FlagsString(self): | 154 def FlagsString(self): |
| 143 # Most flags are 0. | 155 # Most flags are 0. |
| 144 flags = self.flags | 156 flags = self.flags |
| 145 if not flags and not self.aliases: | 157 if not flags and not self.aliases: |
| 146 return '{}' | 158 return '{}' |
| 147 parts = [] | 159 parts = [] |
| 148 if flags & FLAG_ANONYMOUS: | 160 if flags & FLAG_ANONYMOUS: |
| 149 parts.append('anon') | 161 parts.append('anon') |
| 150 if flags & FLAG_STARTUP: | 162 if flags & FLAG_STARTUP: |
| 151 parts.append('startup') | 163 parts.append('startup') |
| 152 if flags & FLAG_UNLIKELY: | 164 if flags & FLAG_UNLIKELY: |
| 153 parts.append('unlikely') | 165 parts.append('unlikely') |
| 154 if flags & FLAG_REL: | 166 if flags & FLAG_REL: |
| 155 parts.append('rel') | 167 parts.append('rel') |
| 156 if flags & FLAG_REL_LOCAL: | 168 if flags & FLAG_REL_LOCAL: |
| 157 parts.append('rel.loc') | 169 parts.append('rel.loc') |
| 170 if flags & FLAG_GENERATED_SOURCE: |
| 171 parts.append('gen') |
| 158 # Not actually a part of flags, but useful to show it here. | 172 # Not actually a part of flags, but useful to show it here. |
| 159 if self.aliases: | 173 if self.aliases: |
| 160 parts.append('{} aliases'.format(self.num_aliases)) | 174 parts.append('{} aliases'.format(self.num_aliases)) |
| 161 return '{%s}' % ','.join(parts) | 175 return '{%s}' % ','.join(parts) |
| 162 | 176 |
| 163 def IsBss(self): | 177 def IsBss(self): |
| 164 return self.section_name == '.bss' | 178 return self.section_name == '.bss' |
| 165 | 179 |
| 166 def IsGroup(self): | 180 def IsGroup(self): |
| 167 return False | 181 return False |
| 168 | 182 |
| 169 def IsGenerated(self): | 183 def IsGeneratedByToolchain(self): |
| 170 # TODO(agrieve): Also match generated functions such as: | 184 return '.' in self.name or ( |
| 171 # startup._GLOBAL__sub_I_page_allocator.cc | 185 self.name.endswith(']') and not self.name.endswith('[]')) |
| 172 return self.name.endswith(']') and not self.name.endswith('[]') | |
| 173 | 186 |
| 174 | 187 |
| 175 class Symbol(BaseSymbol): | 188 class Symbol(BaseSymbol): |
| 176 """Represents a single symbol within a binary. | 189 """Represents a single symbol within a binary. |
| 177 | 190 |
| 178 Refer to module docs for field descriptions. | 191 Refer to module docs for field descriptions. |
| 179 """ | 192 """ |
| 180 | 193 |
| 181 __slots__ = ( | 194 __slots__ = ( |
| 182 'address', | 195 'address', |
| (...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 295 section_name=self.section_name) | 308 section_name=self.section_name) |
| 296 | 309 |
| 297 def __add__(self, other): | 310 def __add__(self, other): |
| 298 self_ids = set(id(s) for s in self) | 311 self_ids = set(id(s) for s in self) |
| 299 after_symbols = self._symbols + [s for s in other if id(s) not in self_ids] | 312 after_symbols = self._symbols + [s for s in other if id(s) not in self_ids] |
| 300 return self._CreateTransformed( | 313 return self._CreateTransformed( |
| 301 after_symbols, section_name=self.section_name, is_sorted=False) | 314 after_symbols, section_name=self.section_name, is_sorted=False) |
| 302 | 315 |
| 303 @property | 316 @property |
| 304 def address(self): | 317 def address(self): |
| 305 first = self._symbols[0].address | 318 first = self._symbols[0].address if self else 0 |
| 306 return first if all(s.address == first for s in self._symbols) else 0 | 319 return first if all(s.address == first for s in self._symbols) else 0 |
| 307 | 320 |
| 308 @property | 321 @property |
| 309 def flags(self): | 322 def flags(self): |
| 310 first = self._symbols[0].flags | 323 first = self._symbols[0].flags if self else 0 |
| 311 return first if all(s.flags == first for s in self._symbols) else 0 | 324 return first if all(s.flags == first for s in self._symbols) else 0 |
| 312 | 325 |
| 313 @property | 326 @property |
| 314 def object_path(self): | 327 def object_path(self): |
| 315 first = self._symbols[0].object_path | 328 first = self._symbols[0].object_path if self else '' |
| 316 return first if all(s.object_path == first for s in self._symbols) else '' | 329 return first if all(s.object_path == first for s in self._symbols) else '' |
| 317 | 330 |
| 318 @property | 331 @property |
| 319 def source_path(self): | 332 def source_path(self): |
| 320 first = self._symbols[0].source_path | 333 first = self._symbols[0].source_path if self else '' |
| 321 return first if all(s.source_path == first for s in self._symbols) else '' | 334 return first if all(s.source_path == first for s in self._symbols) else '' |
| 322 | 335 |
| 323 def IterUniqueSymbols(self): | 336 def IterUniqueSymbols(self): |
| 324 seen_aliases_lists = set() | 337 seen_aliases_lists = set() |
| 325 for s in self: | 338 for s in self: |
| 326 if not s.aliases: | 339 if not s.aliases: |
| 327 yield s | 340 yield s |
| 328 elif id(s.aliases) not in seen_aliases_lists: | 341 elif id(s.aliases) not in seen_aliases_lists: |
| 329 seen_aliases_lists.add(id(s.aliases)) | 342 seen_aliases_lists.add(id(s.aliases)) |
| 330 yield s | 343 yield s |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 377 Subgroups include: | 390 Subgroups include: |
| 378 * Symbols that have [clone] in their name (created during inlining). | 391 * Symbols that have [clone] in their name (created during inlining). |
| 379 * Star symbols (such as "** merge strings", and "** symbol gap") | 392 * Star symbols (such as "** merge strings", and "** symbol gap") |
| 380 | 393 |
| 381 To view created groups: | 394 To view created groups: |
| 382 Print(clustered.Filter(lambda s: s.IsGroup()), recursive=True) | 395 Print(clustered.Filter(lambda s: s.IsGroup()), recursive=True) |
| 383 """ | 396 """ |
| 384 return self._CreateTransformed(cluster_symbols.ClusterSymbols(self)) | 397 return self._CreateTransformed(cluster_symbols.ClusterSymbols(self)) |
| 385 | 398 |
| 386 def Sorted(self, cmp_func=None, key=None, reverse=False): | 399 def Sorted(self, cmp_func=None, key=None, reverse=False): |
| 387 # Default to sorting by abs(size) then name. | |
| 388 if cmp_func is None and key is None: | 400 if cmp_func is None and key is None: |
| 389 cmp_func = lambda a, b: cmp((a.IsBss(), abs(b.size), a.name), | 401 cmp_func = lambda a, b: cmp((a.IsBss(), abs(b.pss), a.name), |
| 390 (b.IsBss(), abs(a.size), b.name)) | 402 (b.IsBss(), abs(a.pss), b.name)) |
| 391 | 403 |
| 392 after_symbols = sorted(self._symbols, cmp_func, key, reverse) | 404 after_symbols = sorted(self._symbols, cmp_func, key, reverse) |
| 393 return self._CreateTransformed( | 405 return self._CreateTransformed( |
| 394 after_symbols, filtered_symbols=self._filtered_symbols, | 406 after_symbols, filtered_symbols=self._filtered_symbols, |
| 395 section_name=self.section_name, is_sorted=True) | 407 section_name=self.section_name, is_sorted=True) |
| 396 | 408 |
| 397 def SortedByName(self, reverse=False): | 409 def SortedByName(self, reverse=False): |
| 398 return self.Sorted(key=(lambda s:s.name), reverse=reverse) | 410 return self.Sorted(key=(lambda s:s.name), reverse=reverse) |
| 399 | 411 |
| 400 def SortedByAddress(self, reverse=False): | 412 def SortedByAddress(self, reverse=False): |
| 401 return self.Sorted(key=(lambda s:s.address), reverse=reverse) | 413 return self.Sorted(key=(lambda s:(s.address, s.object_path, s.name)), |
| 414 reverse=reverse) |
| 402 | 415 |
| 403 def SortedByCount(self, reverse=False): | 416 def SortedByCount(self, reverse=False): |
| 404 return self.Sorted(key=(lambda s:len(s) if s.IsGroup() else 1), | 417 return self.Sorted(key=(lambda s:len(s) if s.IsGroup() else 1), |
| 405 reverse=not reverse) | 418 reverse=not reverse) |
| 406 | 419 |
| 407 def Filter(self, func): | 420 def Filter(self, func): |
| 408 filtered_and_kept = ([], []) | 421 filtered_and_kept = ([], []) |
| 409 symbol = None | 422 symbol = None |
| 410 try: | 423 try: |
| 411 for symbol in self: | 424 for symbol in self: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 423 | 436 |
| 424 def WhereInSection(self, section): | 437 def WhereInSection(self, section): |
| 425 if len(section) == 1: | 438 if len(section) == 1: |
| 426 ret = self.Filter(lambda s: s.section == section) | 439 ret = self.Filter(lambda s: s.section == section) |
| 427 ret.section_name = SECTION_TO_SECTION_NAME[section] | 440 ret.section_name = SECTION_TO_SECTION_NAME[section] |
| 428 else: | 441 else: |
| 429 ret = self.Filter(lambda s: s.section_name == section) | 442 ret = self.Filter(lambda s: s.section_name == section) |
| 430 ret.section_name = section | 443 ret.section_name = section |
| 431 return ret | 444 return ret |
| 432 | 445 |
| 433 def WhereIsGenerated(self): | 446 def WhereSourceIsGenerated(self): |
| 434 return self.Filter(lambda s: s.IsGenerated()) | 447 return self.Filter(lambda s: s.generated_source) |
| 448 |
| 449 def WhereGeneratedByToolchain(self): |
| 450 return self.Filter(lambda s: s.IsGeneratedByToolchain()) |
| 435 | 451 |
| 436 def WhereNameMatches(self, pattern): | 452 def WhereNameMatches(self, pattern): |
| 437 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) | 453 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) |
| 438 return self.Filter(lambda s: regex.search(s.name)) | 454 return self.Filter(lambda s: regex.search(s.name)) |
| 439 | 455 |
| 456 def WhereFullNameMatches(self, pattern): |
| 457 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) |
| 458 return self.Filter(lambda s: regex.search(s.full_name or s.name)) |
| 459 |
| 440 def WhereObjectPathMatches(self, pattern): | 460 def WhereObjectPathMatches(self, pattern): |
| 441 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) | 461 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) |
| 442 return self.Filter(lambda s: regex.search(s.object_path)) | 462 return self.Filter(lambda s: regex.search(s.object_path)) |
| 443 | 463 |
| 444 def WhereSourcePathMatches(self, pattern): | 464 def WhereSourcePathMatches(self, pattern): |
| 445 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) | 465 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) |
| 446 return self.Filter(lambda s: regex.search(s.source_path)) | 466 return self.Filter(lambda s: regex.search(s.source_path)) |
| 447 | 467 |
| 448 def WherePathMatches(self, pattern): | 468 def WherePathMatches(self, pattern): |
| 449 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) | 469 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern)) |
| (...skipping 12 matching lines...) Expand all Loading... |
| 462 """Searches for addesses within [start, end). | 482 """Searches for addesses within [start, end). |
| 463 | 483 |
| 464 Args may be ints or hex strings. Default value for |end| is |start| + 1. | 484 Args may be ints or hex strings. Default value for |end| is |start| + 1. |
| 465 """ | 485 """ |
| 466 if isinstance(start, basestring): | 486 if isinstance(start, basestring): |
| 467 start = int(start, 16) | 487 start = int(start, 16) |
| 468 if end is None: | 488 if end is None: |
| 469 end = start + 1 | 489 end = start + 1 |
| 470 return self.Filter(lambda s: s.address >= start and s.address < end) | 490 return self.Filter(lambda s: s.address >= start and s.address < end) |
| 471 | 491 |
| 492 def WhereHasPath(self): |
| 493 return self.Filter(lambda s: s.source_path or s.object_path) |
| 494 |
| 472 def WhereHasAnyAttribution(self): | 495 def WhereHasAnyAttribution(self): |
| 473 return self.Filter(lambda s: s.name or s.source_path or s.object_path) | 496 return self.Filter(lambda s: s.name or s.source_path or s.object_path) |
| 474 | 497 |
| 475 def Inverted(self): | 498 def Inverted(self): |
| 476 """Returns the symbols that were filtered out by the previous filter. | 499 """Returns the symbols that were filtered out by the previous filter. |
| 477 | 500 |
| 478 Applies only when the previous call was a filter. | 501 Applies only when the previous call was a filter. |
| 479 | 502 |
| 480 Example: | 503 Example: |
| 481 # Symbols that do not have "third_party" in their path. | 504 # Symbols that do not have "third_party" in their path. |
| (...skipping 30 matching lines...) Expand all Loading... |
| 512 min_count = abs(min_count) | 535 min_count = abs(min_count) |
| 513 for token, symbols in symbols_by_token.iteritems(): | 536 for token, symbols in symbols_by_token.iteritems(): |
| 514 if len(symbols) >= min_count: | 537 if len(symbols) >= min_count: |
| 515 after_syms.append(self._CreateTransformed( | 538 after_syms.append(self._CreateTransformed( |
| 516 symbols, name=token, section_name=self.section_name, | 539 symbols, name=token, section_name=self.section_name, |
| 517 is_sorted=False)) | 540 is_sorted=False)) |
| 518 elif include_singles: | 541 elif include_singles: |
| 519 after_syms.extend(symbols) | 542 after_syms.extend(symbols) |
| 520 else: | 543 else: |
| 521 filtered_symbols.extend(symbols) | 544 filtered_symbols.extend(symbols) |
| 522 return self._CreateTransformed( | 545 grouped = self._CreateTransformed( |
| 523 after_syms, filtered_symbols=filtered_symbols, | 546 after_syms, filtered_symbols=filtered_symbols, |
| 524 section_name=self.section_name, is_sorted=False) | 547 section_name=self.section_name, is_sorted=False) |
| 548 # Grouping is rarely an intermediate step, so assume sorting is useful. |
| 549 return grouped.Sorted() |
| 525 | 550 |
| 526 def GroupBySectionName(self): | 551 def GroupBySectionName(self): |
| 527 return self.GroupBy(lambda s: s.section_name) | 552 return self.GroupBy(lambda s: s.section_name) |
| 528 | 553 |
| 529 def GroupByNamespace(self, depth=0, fallback='{global}', min_count=0): | 554 def GroupByNamespace(self, depth=0, fallback='{global}', min_count=0): |
| 530 """Groups by symbol namespace (as denoted by ::s). | 555 """Groups by symbol namespace (as denoted by ::s). |
| 531 | 556 |
| 532 Does not differentiate between C++ namespaces and C++ classes. | 557 Does not differentiate between C++ namespaces and C++ classes. |
| 533 | 558 |
| 534 Args: | 559 Args: |
| (...skipping 14 matching lines...) Expand all Loading... |
| 549 | 574 |
| 550 # Remove after the final :: (not part of the namespace). | 575 # Remove after the final :: (not part of the namespace). |
| 551 colon_idx = name.rfind('::') | 576 colon_idx = name.rfind('::') |
| 552 if colon_idx == -1: | 577 if colon_idx == -1: |
| 553 return fallback | 578 return fallback |
| 554 name = name[:colon_idx] | 579 name = name[:colon_idx] |
| 555 | 580 |
| 556 return _ExtractPrefixBeforeSeparator(name, '::', depth) | 581 return _ExtractPrefixBeforeSeparator(name, '::', depth) |
| 557 return self.GroupBy(extract_namespace, min_count=min_count) | 582 return self.GroupBy(extract_namespace, min_count=min_count) |
| 558 | 583 |
| 559 def GroupBySourcePath(self, depth=0, fallback='{no path}', | 584 def GroupByPath(self, depth=0, fallback='{no path}', |
| 560 fallback_to_object_path=True, min_count=0): | 585 fallback_to_object_path=True, min_count=0): |
| 561 """Groups by source_path. | 586 """Groups by source_path. |
| 562 | 587 |
| 563 Args: | 588 Args: |
| 564 depth: When 0 (default), groups by entire path. When 1, groups by | 589 depth: When 0 (default), groups by entire path. When 1, groups by |
| 565 top-level directory, when 2, groups by top 2 directories, etc. | 590 top-level directory, when 2, groups by top 2 directories, etc. |
| 566 fallback: Use this value when no namespace exists. | 591 fallback: Use this value when no namespace exists. |
| 567 fallback_to_object_path: When True (default), uses object_path when | 592 fallback_to_object_path: When True (default), uses object_path when |
| 568 source_path is missing. | 593 source_path is missing. |
| 569 min_count: Miniumum number of symbols for a group. If fewer than this many | 594 min_count: Miniumum number of symbols for a group. If fewer than this many |
| 570 symbols end up in a group, they will not be put within a group. | 595 symbols end up in a group, they will not be put within a group. |
| 571 Use a negative value to omit symbols entirely rather than | 596 Use a negative value to omit symbols entirely rather than |
| 572 include them outside of a group. | 597 include them outside of a group. |
| 573 """ | 598 """ |
| 574 def extract_path(symbol): | 599 def extract_path(symbol): |
| 575 path = symbol.source_path | 600 path = symbol.source_path |
| 576 if fallback_to_object_path and not path: | 601 if fallback_to_object_path and not path: |
| 577 path = symbol.object_path | 602 path = symbol.object_path |
| 578 path = path or fallback | 603 path = path or fallback |
| 579 return _ExtractPrefixBeforeSeparator(path, os.path.sep, depth) | 604 return _ExtractPrefixBeforeSeparator(path, os.path.sep, depth) |
| 580 return self.GroupBy(extract_path, min_count=min_count) | 605 return self.GroupBy(extract_path, min_count=min_count) |
| 581 | 606 |
| 582 def GroupByObjectPath(self, depth=0, fallback='{no path}', min_count=0): | |
| 583 """Groups by object_path. | |
| 584 | |
| 585 Args: | |
| 586 depth: When 0 (default), groups by entire path. When 1, groups by | |
| 587 top-level directory, when 2, groups by top 2 directories, etc. | |
| 588 fallback: Use this value when no namespace exists. | |
| 589 min_count: Miniumum number of symbols for a group. If fewer than this many | |
| 590 symbols end up in a group, they will not be put within a group. | |
| 591 Use a negative value to omit symbols entirely rather than | |
| 592 include them outside of a group. | |
| 593 """ | |
| 594 def extract_path(symbol): | |
| 595 path = symbol.object_path or fallback | |
| 596 return _ExtractPrefixBeforeSeparator(path, os.path.sep, depth) | |
| 597 return self.GroupBy(extract_path, min_count=min_count) | |
| 598 | |
| 599 | 607 |
| 600 class SymbolDiff(SymbolGroup): | 608 class SymbolDiff(SymbolGroup): |
| 601 """A SymbolGroup subclass representing a diff of two other SymbolGroups. | 609 """A SymbolGroup subclass representing a diff of two other SymbolGroups. |
| 602 | 610 |
| 603 All Symbols contained within have a |size| which is actually the size delta. | 611 All Symbols contained within have a |size| which is actually the size delta. |
| 604 Additionally, metadata is kept about which symbols were added / removed / | 612 Additionally, metadata is kept about which symbols were added / removed / |
| 605 changed. | 613 changed. |
| 606 """ | 614 """ |
| 607 __slots__ = ( | 615 __slots__ = ( |
| 608 '_added_ids', | 616 '_added_ids', |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 697 | 705 |
| 698 def _ExtractPrefixBeforeSeparator(string, separator, count=1): | 706 def _ExtractPrefixBeforeSeparator(string, separator, count=1): |
| 699 idx = -len(separator) | 707 idx = -len(separator) |
| 700 prev_idx = None | 708 prev_idx = None |
| 701 for _ in xrange(count): | 709 for _ in xrange(count): |
| 702 idx = string.find(separator, idx + len(separator)) | 710 idx = string.find(separator, idx + len(separator)) |
| 703 if idx < 0: | 711 if idx < 0: |
| 704 break | 712 break |
| 705 prev_idx = idx | 713 prev_idx = idx |
| 706 return string[:prev_idx] | 714 return string[:prev_idx] |
| OLD | NEW |