Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(296)

Side by Side Diff: tools/binary_size/models.py

Issue 2813963002: //tools/binary_size: Consolidate most tools into "supersize" command (Closed)
Patch Set: Fix readme formatting. Make archive's --outoput-file a positional arg Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tools/binary_size/match_util_test.py ('k') | tools/binary_size/ninja_parser.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 # found in the LICENSE file.
4 """Classes that comprise the data model for binary size analysis.
5
6 The primary classes are Symbol, and SymbolGroup.
7
8 Description of common properties:
9 * address: The start address of the symbol.
10 May be 0 (e.g. for .bss or for SymbolGroups).
11 * size: The number of bytes this symbol takes up, including padding that comes
12 before |address|.
13 * padding: The number of bytes of padding before |address| due to this symbol.
14 * name: Symbol names with parameter list removed.
15 Never None, but will be '' for anonymous symbols.
16 * full_name: Symbols names with parameter list left in.
17 Never None, but will be '' for anonymous symbols, and for symbols that do
18 not contain a parameter list.
19 * is_anonymous: True when the symbol exists in an anonymous namespace (which
20 are removed from both full_name and name during normalization).
21 * section_name: E.g. ".text", ".rodata", ".data.rel.local"
22 * section: The second character of |section_name|. E.g. "t", "r", "d".
23 """
24
25 import collections
26 import copy
27 import os
28 import re
29
30 import match_util
31
32
33 METADATA_GIT_REVISION = 'git_revision'
34 METADATA_MAP_FILENAME = 'map_file_name' # Path relative to output_directory.
35 METADATA_ELF_FILENAME = 'elf_file_name' # Path relative to output_directory.
36 METADATA_ELF_MTIME = 'elf_mtime' # int timestamp in utc.
37 METADATA_ELF_BUILD_ID = 'elf_build_id'
38 METADATA_GN_ARGS = 'gn_args'
39
40
41 SECTION_TO_SECTION_NAME = {
42 'b': '.bss',
43 'd': '.data',
44 'r': '.rodata',
45 't': '.text',
46 }
47
48
49 class SizeInfo(object):
50 """Represents all size information for a single binary.
51
52 Fields:
53 section_sizes: A dict of section_name -> size.
54 raw_symbols: A flat list of all symbols.
55 symbols: A SymbolGroup containing raw_symbols, but with some Symbols grouped
56 into sub-SymbolGroups.
57 metadata: A dict.
58 """
59 __slots__ = (
60 'section_sizes',
61 'raw_symbols',
62 'symbols',
63 'metadata',
64 )
65
66 """Root size information."""
67 def __init__(self, section_sizes, raw_symbols, grouped_symbols=None,
68 metadata=None):
69 self.section_sizes = section_sizes # E.g. {'.text': 0}
70 # List of symbols sorted by address per-section.
71 self.raw_symbols = raw_symbols
72 # Root SymbolGroup. Cloned symbols grouped together within sub-SymbolGroups.
73 self.symbols = grouped_symbols
74 self.metadata = metadata or {}
75
76
77 class SizeInfoDiff(object):
78 """What you get when you Diff() two SizeInfo objects.
79
80 Fields:
81 section_sizes: A dict of section_name -> size delta.
82 symbols: A SymbolDiff with all symbols in it.
83 old_metadata: metadata of the "old" SizeInfo.
84 new_metadata: metadata of the "new" SizeInfo.
85 """
86 __slots__ = (
87 'section_sizes',
88 'symbols',
89 'old_metadata',
90 'new_metadata',
91 )
92
93 def __init__(self, section_sizes, symbols, old_metadata, new_metadata):
94 self.section_sizes = section_sizes
95 self.symbols = symbols
96 self.old_metadata = old_metadata
97 self.new_metadata = new_metadata
98
99
100 class BaseSymbol(object):
101 """Base class for Symbol and SymbolGroup.
102
103 Refer to module docs for field descriptions.
104 """
105 __slots__ = ()
106
107 @property
108 def section(self):
109 """Returns the one-letter section.
110
111 E.g. If section_name == '.rodata', then section == 'r'.
112 """
113 return self.section_name[1]
114
115 @property
116 def size_without_padding(self):
117 return self.size - self.padding
118
119 @property
120 def end_address(self):
121 return self.address + self.size_without_padding
122
123 def IsBss(self):
124 return self.section_name == '.bss'
125
126 def IsGroup(self):
127 return False
128
129 def IsGenerated(self):
130 # TODO(agrieve): Also match generated functions such as:
131 # startup._GLOBAL__sub_I_page_allocator.cc
132 return self.name.endswith(']') and not self.name.endswith('[]')
133
134 def _Key(self):
135 """Returns a tuple that can be used to see if two Symbol are the same.
136
137 Keys are not guaranteed to be unique within a SymbolGroup. For example, it
138 is common to have multiple "** merge strings" symbols, which will have a
139 common key."""
140 stripped_full_name = self.full_name
141 if stripped_full_name:
142 clone_idx = stripped_full_name.find(' [clone ')
143 if clone_idx != -1:
144 stripped_full_name = stripped_full_name[:clone_idx]
145 return (self.section_name, stripped_full_name or self.name)
146
147
148 class Symbol(BaseSymbol):
149 """Represents a single symbol within a binary.
150
151 Refer to module docs for field descriptions.
152 """
153
154 __slots__ = (
155 'address',
156 'full_name',
157 'is_anonymous',
158 'object_path',
159 'name',
160 'padding',
161 'section_name',
162 'source_path',
163 'size',
164 )
165
166 def __init__(self, section_name, size_without_padding, address=None,
167 name=None, source_path=None, object_path=None,
168 full_name=None, is_anonymous=False):
169 self.section_name = section_name
170 self.address = address or 0
171 self.name = name or ''
172 self.full_name = full_name or ''
173 self.source_path = source_path or ''
174 self.object_path = object_path or ''
175 self.size = size_without_padding
176 # Change this to be a bitfield of flags if ever there is a need to add
177 # another similar thing.
178 self.is_anonymous = is_anonymous
179 self.padding = 0
180
181 def __repr__(self):
182 return ('%s@%x(size_without_padding=%d,padding=%d,name=%s,path=%s,anon=%d)'
183 % (self.section_name, self.address, self.size_without_padding,
184 self.padding, self.name, self.source_path or self.object_path,
185 int(self.is_anonymous)))
186
187
188 class SymbolGroup(BaseSymbol):
189 """Represents a group of symbols using the same interface as Symbol.
190
191 SymbolGroups are immutable. All filtering / sorting will return new
192 SymbolGroups objects.
193
194 Overrides many __functions__. E.g. the following are all valid:
195 * len(group)
196 * iter(group)
197 * group[0]
198 * group['0x1234'] # By symbol address
199 * without_group2 = group1 - group2
200 * unioned = group1 + group2
201 """
202
203 __slots__ = (
204 '_padding',
205 '_size',
206 '_symbols',
207 '_filtered_symbols',
208 'full_name',
209 'name',
210 'section_name',
211 'is_sorted',
212 )
213
214 def __init__(self, symbols, filtered_symbols=None, name=None,
215 full_name=None, section_name=None, is_sorted=False):
216 self._padding = None
217 self._size = None
218 self._symbols = symbols
219 self._filtered_symbols = filtered_symbols or []
220 self.name = name or ''
221 self.full_name = full_name
222 self.section_name = section_name or '.*'
223 self.is_sorted = is_sorted
224
225 def __repr__(self):
226 return 'Group(name=%s,count=%d,size=%d)' % (
227 self.name, len(self), self.size)
228
229 def __iter__(self):
230 return iter(self._symbols)
231
232 def __len__(self):
233 return len(self._symbols)
234
235 def __eq__(self, other):
236 return self._symbols == other._symbols
237
238 def __getitem__(self, key):
239 """|key| can be an index or an address.
240
241 Raises if multiple symbols map to the address.
242 """
243 if isinstance(key, slice):
244 return self._symbols.__getitem__(key)
245 if isinstance(key, basestring) or key > len(self._symbols):
246 found = self.WhereAddressInRange(key)
247 if len(found) != 1:
248 raise KeyError('%d symbols found at address %s.' % (len(found), key))
249 return found[0]
250 return self._symbols[key]
251
252 def __sub__(self, other):
253 other_ids = set(id(s) for s in other)
254 new_symbols = [s for s in self if id(s) not in other_ids]
255 return self._CreateTransformed(new_symbols, section_name=self.section_name)
256
257 def __add__(self, other):
258 self_ids = set(id(s) for s in self)
259 new_symbols = self._symbols + [s for s in other if id(s) not in self_ids]
260 return self._CreateTransformed(new_symbols, section_name=self.section_name,
261 is_sorted=False)
262
263 @property
264 def address(self):
265 first = self._symbols[0].address
266 return first if all(s.address == first for s in self._symbols) else 0
267
268 @property
269 def is_anonymous(self):
270 first = self._symbols[0].is_anonymous
271 return first if all(
272 s.is_anonymous == first for s in self._symbols) else False
273
274 @property
275 def object_path(self):
276 first = self._symbols[0].object_path
277 return first if all(s.object_path == first for s in self._symbols) else None
278
279 @property
280 def source_path(self):
281 first = self._symbols[0].source_path
282 return first if all(s.source_path == first for s in self._symbols) else None
283
284 @property
285 def size(self):
286 if self._size is None:
287 if self.IsBss():
288 self._size = sum(s.size for s in self)
289 self._size = sum(s.size for s in self if not s.IsBss())
290 return self._size
291
292 @property
293 def padding(self):
294 if self._padding is None:
295 self._padding = sum(s.padding for s in self)
296 return self._padding
297
298 def IsGroup(self):
299 return True
300
301 def _CreateTransformed(self, symbols, filtered_symbols=None, name=None,
302 section_name=None, is_sorted=None):
303 if is_sorted is None:
304 is_sorted = self.is_sorted
305 return SymbolGroup(symbols, filtered_symbols=filtered_symbols, name=name,
306 section_name=section_name, is_sorted=is_sorted)
307
308 def Sorted(self, cmp_func=None, key=None, reverse=False):
309 # Default to sorting by abs(size) then name.
310 if cmp_func is None and key is None:
311 cmp_func = lambda a, b: cmp((a.IsBss(), abs(b.size), a.name),
312 (b.IsBss(), abs(a.size), b.name))
313
314 new_symbols = sorted(self._symbols, cmp_func, key, reverse)
315 return self._CreateTransformed(
316 new_symbols, filtered_symbols=self._filtered_symbols,
317 section_name=self.section_name, is_sorted=True)
318
319 def SortedByName(self, reverse=False):
320 return self.Sorted(key=(lambda s:s.name), reverse=reverse)
321
322 def SortedByAddress(self, reverse=False):
323 return self.Sorted(key=(lambda s:s.address), reverse=reverse)
324
325 def SortedByCount(self, reverse=False):
326 return self.Sorted(key=(lambda s:len(s) if s.IsGroup() else 1),
327 reverse=not reverse)
328
329 def Filter(self, func):
330 filtered_and_kept = ([], [])
331 for symbol in self:
332 filtered_and_kept[int(bool(func(symbol)))].append(symbol)
333 return self._CreateTransformed(filtered_and_kept[1],
334 filtered_symbols=filtered_and_kept[0],
335 section_name=self.section_name)
336
337 def WhereBiggerThan(self, min_size):
338 return self.Filter(lambda s: s.size >= min_size)
339
340 def WhereInSection(self, section):
341 if len(section) == 1:
342 ret = self.Filter(lambda s: s.section == section)
343 ret.section_name = SECTION_TO_SECTION_NAME[section]
344 else:
345 ret = self.Filter(lambda s: s.section_name == section)
346 ret.section_name = section
347 return ret
348
349 def WhereIsGenerated(self):
350 return self.Filter(lambda s: s.IsGenerated())
351
352 def WhereNameMatches(self, pattern):
353 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern))
354 return self.Filter(lambda s: regex.search(s.name))
355
356 def WhereObjectPathMatches(self, pattern):
357 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern))
358 return self.Filter(lambda s: regex.search(s.object_path))
359
360 def WhereSourcePathMatches(self, pattern):
361 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern))
362 return self.Filter(lambda s: regex.search(s.source_path))
363
364 def WherePathMatches(self, pattern):
365 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern))
366 return self.Filter(lambda s: (regex.search(s.source_path) or
367 regex.search(s.object_path)))
368
369 def WhereMatches(self, pattern):
370 """Looks for |pattern| within all paths & names."""
371 regex = re.compile(match_util.ExpandRegexIdentifierPlaceholder(pattern))
372 return self.Filter(lambda s: (regex.search(s.source_path) or
373 regex.search(s.object_path) or
374 regex.search(s.full_name or '') or
375 regex.search(s.name)))
376
377 def WhereAddressInRange(self, start, end=None):
378 """Searches for addesses within [start, end).
379
380 Args may be ints or hex strings. Default value for |end| is |start| + 1.
381 """
382 if isinstance(start, basestring):
383 start = int(start, 16)
384 if end is None:
385 end = start + 1
386 return self.Filter(lambda s: s.address >= start and s.address < end)
387
388 def WhereHasAnyAttribution(self):
389 return self.Filter(lambda s: s.name or s.source_path or s.object_path)
390
391 def Inverted(self):
392 """Returns the symbols that were filtered out by the previous filter.
393
394 Applies only when the previous call was a filter.
395
396 Example:
397 # Symbols that do not have "third_party" in their path.
398 symbols.WherePathMatches(r'third_party').Inverted()
399 # Symbols within third_party that do not contain the string "foo".
400 symbols.WherePathMatches(r'third_party').WhereMatches('foo').Inverted()
401 """
402 return self._CreateTransformed(
403 self._filtered_symbols, filtered_symbols=self._symbols, is_sorted=False)
404
405 def GroupBy(self, func, min_count=0):
406 """Returns a SymbolGroup of SymbolGroups, indexed by |func|.
407
408 Args:
409 func: Grouping function. Passed a symbol and returns a string for the
410 name of the subgroup to put the symbol in. If None is returned, the
411 symbol is omitted.
412 min_count: Miniumum number of symbols for a group. If fewer than this many
413 symbols end up in a group, they will not be put within a group.
414 Use a negative value to omit symbols entirely rather than
415 include them outside of a group.
416 """
417 new_syms = []
418 filtered_symbols = []
419 symbols_by_token = collections.defaultdict(list)
420 # Index symbols by |func|.
421 for symbol in self:
422 token = func(symbol)
423 if token is None:
424 filtered_symbols.append(symbol)
425 symbols_by_token[token].append(symbol)
426 # Create the subgroups.
427 include_singles = min_count >= 0
428 min_count = abs(min_count)
429 for token, symbols in symbols_by_token.iteritems():
430 if len(symbols) >= min_count:
431 new_syms.append(self._CreateTransformed(
432 symbols, name=token, section_name=self.section_name,
433 is_sorted=False))
434 elif include_singles:
435 new_syms.extend(symbols)
436 else:
437 filtered_symbols.extend(symbols)
438 return self._CreateTransformed(
439 new_syms, filtered_symbols=filtered_symbols,
440 section_name=self.section_name, is_sorted=False)
441
442 def GroupBySectionName(self):
443 return self.GroupBy(lambda s: s.section_name)
444
445 def GroupByNamespace(self, depth=0, fallback='{global}', min_count=0):
446 """Groups by symbol namespace (as denoted by ::s).
447
448 Does not differentiate between C++ namespaces and C++ classes.
449
450 Args:
451 depth: When 0 (default), groups by entire namespace. When 1, groups by
452 top-level name, when 2, groups by top 2 names, etc.
453 fallback: Use this value when no namespace exists.
454 min_count: Miniumum number of symbols for a group. If fewer than this many
455 symbols end up in a group, they will not be put within a group.
456 Use a negative value to omit symbols entirely rather than
457 include them outside of a group.
458 """
459 def extract_namespace(symbol):
460 # Remove template params.
461 name = symbol.name
462 template_idx = name.find('<')
463 if template_idx:
464 name = name[:template_idx]
465
466 # Remove after the final :: (not part of the namespace).
467 colon_idx = name.rfind('::')
468 if colon_idx == -1:
469 return fallback
470 name = name[:colon_idx]
471
472 return _ExtractPrefixBeforeSeparator(name, '::', depth)
473 return self.GroupBy(extract_namespace, min_count=min_count)
474
475 def GroupBySourcePath(self, depth=0, fallback='{no path}',
476 fallback_to_object_path=True, min_count=0):
477 """Groups by source_path.
478
479 Args:
480 depth: When 0 (default), groups by entire path. When 1, groups by
481 top-level directory, when 2, groups by top 2 directories, etc.
482 fallback: Use this value when no namespace exists.
483 fallback_to_object_path: When True (default), uses object_path when
484 source_path is missing.
485 min_count: Miniumum number of symbols for a group. If fewer than this many
486 symbols end up in a group, they will not be put within a group.
487 Use a negative value to omit symbols entirely rather than
488 include them outside of a group.
489 """
490 def extract_path(symbol):
491 path = symbol.source_path
492 if fallback_to_object_path and not path:
493 path = symbol.object_path
494 path = path or fallback
495 return _ExtractPrefixBeforeSeparator(path, os.path.sep, depth)
496 return self.GroupBy(extract_path, min_count=min_count)
497
498 def GroupByObjectPath(self, depth=0, fallback='{no path}', min_count=0):
499 """Groups by object_path.
500
501 Args:
502 depth: When 0 (default), groups by entire path. When 1, groups by
503 top-level directory, when 2, groups by top 2 directories, etc.
504 fallback: Use this value when no namespace exists.
505 min_count: Miniumum number of symbols for a group. If fewer than this many
506 symbols end up in a group, they will not be put within a group.
507 Use a negative value to omit symbols entirely rather than
508 include them outside of a group.
509 """
510 def extract_path(symbol):
511 path = symbol.object_path or fallback
512 return _ExtractPrefixBeforeSeparator(path, os.path.sep, depth)
513 return self.GroupBy(extract_path, min_count=min_count)
514
515
516 class SymbolDiff(SymbolGroup):
517 """A SymbolGroup subclass representing a diff of two other SymbolGroups.
518
519 All Symbols contained within have a |size| which is actually the size delta.
520 Additionally, metadata is kept about which symbols were added / removed /
521 changed.
522 """
523 __slots__ = (
524 '_added_ids',
525 '_removed_ids',
526 )
527
528 def __init__(self, added, removed, similar, name=None, full_name=None,
529 section_name=None):
530 self._added_ids = set(id(s) for s in added)
531 self._removed_ids = set(id(s) for s in removed)
532 symbols = []
533 symbols.extend(added)
534 symbols.extend(removed)
535 symbols.extend(similar)
536 super(SymbolDiff, self).__init__(symbols, name=name, full_name=full_name,
537 section_name=section_name)
538
539 def __repr__(self):
540 return '%s(%d added, %d removed, %d changed, %d unchanged, size=%d)' % (
541 'SymbolGroup', self.added_count, self.removed_count, self.changed_count,
542 self.unchanged_count, self.size)
543
544 def _CreateTransformed(self, symbols, filtered_symbols=None, name=None,
545 section_name=None, is_sorted=None):
546 ret = SymbolDiff.__new__(SymbolDiff)
547 # Printing sorts, so fast-path the same symbols case.
548 if len(symbols) == len(self._symbols):
549 ret._added_ids = self._added_ids
550 ret._removed_ids = self._removed_ids
551 else:
552 ret._added_ids = set(id(s) for s in symbols if self.IsAdded(s))
553 ret._removed_ids = set(id(s) for s in symbols if self.IsRemoved(s))
554 super(SymbolDiff, ret).__init__(
555 symbols, filtered_symbols=filtered_symbols, name=name,
556 section_name=section_name, is_sorted=is_sorted)
557 return ret
558
559 @property
560 def added_count(self):
561 return len(self._added_ids)
562
563 @property
564 def removed_count(self):
565 return len(self._removed_ids)
566
567 @property
568 def changed_count(self):
569 not_changed = self.unchanged_count + self.added_count + self.removed_count
570 return len(self) - not_changed
571
572 @property
573 def unchanged_count(self):
574 return sum(1 for s in self if self.IsSimilar(s) and s.size == 0)
575
576 def IsAdded(self, sym):
577 return id(sym) in self._added_ids
578
579 def IsSimilar(self, sym):
580 key = id(sym)
581 return key not in self._added_ids and key not in self._removed_ids
582
583 def IsRemoved(self, sym):
584 return id(sym) in self._removed_ids
585
586 def WhereNotUnchanged(self):
587 return self.Filter(lambda s: not self.IsSimilar(s) or s.size)
588
589
590 def Diff(new, old):
591 """Diffs two SizeInfo or SymbolGroup objects.
592
593 When diffing SizeInfos, a SizeInfoDiff is returned.
594 When diffing SymbolGroups, a SymbolDiff is returned.
595
596 Returns:
597 Returns a SizeInfo when args are of type SizeInfo.
598 Returns a SymbolDiff when args are of type SymbolGroup.
599 """
600 if isinstance(new, SizeInfo):
601 assert isinstance(old, SizeInfo)
602 section_sizes = {
603 k:new.section_sizes[k] - v for k, v in old.section_sizes.iteritems()}
604 symbol_diff = Diff(new.symbols, old.symbols)
605 return SizeInfoDiff(section_sizes, symbol_diff, old.metadata, new.metadata)
606
607 assert isinstance(new, SymbolGroup) and isinstance(old, SymbolGroup)
608 return _DiffSymbols(new, old)
609
610
611 def _NegateAll(symbols):
612 ret = []
613 for symbol in symbols:
614 if symbol.IsGroup():
615 duped = SymbolDiff([], _NegateAll(symbol), [], name=symbol.name,
616 full_name=symbol.full_name,
617 section_name=symbol.section_name)
618 else:
619 duped = copy.copy(symbol)
620 duped.size = -duped.size
621 duped.padding = -duped.padding
622 ret.append(duped)
623 return ret
624
625
626 def _DiffSymbols(new_group, old_group):
627 symbols_by_key = collections.defaultdict(list)
628 for s in old_group:
629 symbols_by_key[s._Key()].append(s)
630
631 added = []
632 similar = []
633 # For similar symbols, padding is zeroed out. In order to not lose the
634 # information entirely, store it in aggregate.
635 padding_by_section_name = collections.defaultdict(int)
636 for new_sym in new_group:
637 matching_syms = symbols_by_key.get(new_sym._Key())
638 if matching_syms:
639 old_sym = matching_syms.pop(0)
640 if old_sym.IsGroup() and new_sym.IsGroup():
641 merged_sym = _DiffSymbols(new_sym, old_sym)
642 else:
643 size_diff = new_sym.size_without_padding - old_sym.size_without_padding
644 merged_sym = Symbol(new_sym.section_name, size_diff,
645 address=new_sym.address, name=new_sym.name,
646 source_path=new_sym.source_path,
647 object_path=new_sym.object_path,
648 full_name=new_sym.full_name,
649 is_anonymous=new_sym.is_anonymous)
650
651 # Diffs are more stable when comparing size without padding, except when
652 # the symbol is a padding-only symbol.
653 if new_sym.size_without_padding == 0 and size_diff == 0:
654 merged_sym.padding = new_sym.padding - old_sym.padding
655 else:
656 padding_by_section_name[new_sym.section_name] += (
657 new_sym.padding - old_sym.padding)
658
659 similar.append(merged_sym)
660 else:
661 added.append(new_sym)
662
663 removed = []
664 for remaining_syms in symbols_by_key.itervalues():
665 if remaining_syms:
666 removed.extend(_NegateAll(remaining_syms))
667
668 for section_name, padding in padding_by_section_name.iteritems():
669 if padding != 0:
670 similar.append(Symbol(section_name, padding,
671 name="** aggregate padding of diff'ed symbols"))
672 return SymbolDiff(added, removed, similar, name=new_group.name,
673 full_name=new_group.full_name,
674 section_name=new_group.section_name)
675
676
677 def _ExtractPrefixBeforeSeparator(string, separator, count=1):
678 idx = -len(separator)
679 prev_idx = None
680 for _ in xrange(count):
681 idx = string.find(separator, idx + len(separator))
682 if idx < 0:
683 break
684 prev_idx = idx
685 return string[:prev_idx]
OLDNEW
« no previous file with comments | « tools/binary_size/match_util_test.py ('k') | tools/binary_size/ninja_parser.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698