view i18n/check-translation.py @ 35116:da91e7309daf

bundle2: don't use seekable bundle2 parts by default (issue5691) The last commit removed the last use of the bundle2 part seek() API in the generic bundle2 part iteration code. This means we can now switch to using unseekable bundle2 parts by default and have the special consumers that actually need the behavior request it. This commit changes unbundle20.iterparts() to expose non-seekable unbundlepart instances by default. If seekable parts are needed, callers can pass "seekable=True." The bundlerepo class needs seekable parts, so it does this. The interrupt handler is also changed to use a regular unbundlepart. So, by default, all consumers except bundlerepo will see unseekable parts. Because the behavior of the iterparts() benchmark changed, we add a variation to test seekable parts vs unseekable parts. And because parts no longer have seek() unless "seekable=True," we update the "part seek" benchmark. Speaking of benchmarks, this change has the following impact to `hg perfbundleread` on an uncompressed bundle of the Firefox repo (6,070,036,163 bytes): ! read(8k) ! wall 0.722709 comb 0.720000 user 0.150000 sys 0.570000 (best of 14) ! read(16k) ! wall 0.602208 comb 0.590000 user 0.080000 sys 0.510000 (best of 17) ! read(32k) ! wall 0.554018 comb 0.560000 user 0.050000 sys 0.510000 (best of 18) ! read(128k) ! wall 0.520086 comb 0.530000 user 0.020000 sys 0.510000 (best of 20) ! bundle2 forwardchunks() ! wall 2.996329 comb 3.000000 user 2.300000 sys 0.700000 (best of 4) ! bundle2 iterparts() ! wall 8.070791 comb 8.060000 user 7.180000 sys 0.880000 (best of 3) ! wall 6.983756 comb 6.980000 user 6.220000 sys 0.760000 (best of 3) ! bundle2 iterparts() seekable ! wall 8.132131 comb 8.110000 user 7.160000 sys 0.950000 (best of 3) ! bundle2 part seek() ! wall 10.370142 comb 10.350000 user 7.430000 sys 2.920000 (best of 3) ! wall 10.860942 comb 10.840000 user 7.790000 sys 3.050000 (best of 3) ! bundle2 part read(8k) ! wall 8.599892 comb 8.580000 user 7.720000 sys 0.860000 (best of 3) ! wall 7.258035 comb 7.260000 user 6.470000 sys 0.790000 (best of 3) ! bundle2 part read(16k) ! wall 8.265361 comb 8.250000 user 7.360000 sys 0.890000 (best of 3) ! wall 7.099891 comb 7.080000 user 6.310000 sys 0.770000 (best of 3) ! bundle2 part read(32k) ! wall 8.290308 comb 8.280000 user 7.330000 sys 0.950000 (best of 3) ! wall 6.964685 comb 6.950000 user 6.130000 sys 0.820000 (best of 3) ! bundle2 part read(128k) ! wall 8.204900 comb 8.150000 user 7.210000 sys 0.940000 (best of 3) ! wall 6.852867 comb 6.850000 user 6.060000 sys 0.790000 (best of 3) The significant speedup is due to not incurring the overhead to track payload offset data. Of course, this overhead is proportional to bundle2 part size. So a multiple gigabyte changegroup part is on the extreme side of the spectrum for real-world impact. In addition to the CPU efficiency wins, not tracking offset data also means not using memory to hold that data. Using a bundle based on the example BSD repository in issue 5691, this change has a drastic impact to memory usage during `hg unbundle` (`hg clone` would behave similarly). Before, memory usage incrementally increased for the duration of bundle processing. In other words, as we advanced through the changegroup and bundle2 part, we kept allocating more memory to hold offset data. After this change, we still increase memory during changegroup application. But the rate of increase is significantly slower. (A bulk of the remaining gradual increase appears to be the storing of revlog sizes in the transaction object to facilitate rollback.) The RSS at the end of filelog application is as follows: Before: ~752 MB After: ~567 MB So, we were storing ~185 MB of offset data that we never even used. Talk about wasteful! .. api:: bundle2 parts are no longer seekable by default. .. perf:: bundle2 read I/O throughput significantly increased. .. perf:: Significant memory use reductions when reading from bundle2 bundles. On the BSD repository, peak RSS during changegroup application decreased by ~185 MB from ~752 MB to ~567 MB. Differential Revision: https://phab.mercurial-scm.org/D1390
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 13 Nov 2017 21:10:37 -0800
parents 078099304772
children 19fc5a986669
line wrap: on
line source

#!/usr/bin/env python
#
# check-translation.py - check Mercurial specific translation problems
from __future__ import absolute_import

import re

import polib

scanners = []
checkers = []

def scanner():
    def decorator(func):
        scanners.append(func)
        return func
    return decorator

def levelchecker(level, msgidpat):
    def decorator(func):
        if msgidpat:
            match = re.compile(msgidpat).search
        else:
            match = lambda msgid: True
        checkers.append((func, level))
        func.match = match
        return func
    return decorator

def match(checker, pe):
    """Examine whether POEntry "pe" is target of specified checker or not
    """
    if not checker.match(pe.msgid):
        return
    # examine suppression by translator comment
    nochecker = 'no-%s-check' % checker.__name__
    for tc in pe.tcomment.split():
        if nochecker == tc:
            return
    return True

####################

def fatalchecker(msgidpat=None):
    return levelchecker('fatal', msgidpat)

@fatalchecker(r'\$\$')
def promptchoice(pe):
    """Check translation of the string given to "ui.promptchoice()"

    >>> pe = polib.POEntry(
    ...     msgid ='prompt$$missing &sep$$missing &amp$$followed by &none',
    ...     msgstr='prompt  missing &sep$$missing  amp$$followed by none&')
    >>> match(promptchoice, pe)
    True
    >>> for e in promptchoice(pe): print(e)
    number of choices differs between msgid and msgstr
    msgstr has invalid choice missing '&'
    msgstr has invalid '&' followed by none
    """
    idchoices = [c.rstrip(' ') for c in pe.msgid.split('$$')[1:]]
    strchoices = [c.rstrip(' ') for c in pe.msgstr.split('$$')[1:]]

    if len(idchoices) != len(strchoices):
        yield "number of choices differs between msgid and msgstr"

    indices = [(c, c.find('&')) for c in strchoices]
    if [c for c, i in indices if i == -1]:
        yield "msgstr has invalid choice missing '&'"
    if [c for c, i in indices if len(c) == i + 1]:
        yield "msgstr has invalid '&' followed by none"

deprecatedpe = None
@scanner()
def deprecatedsetup(pofile):
    pes = [p for p in pofile if p.msgid == '(DEPRECATED)' and p.msgstr]
    if len(pes):
        global deprecatedpe
        deprecatedpe = pes[0]

@fatalchecker(r'\(DEPRECATED\)')
def deprecated(pe):
    """Check for DEPRECATED
    >>> ped = polib.POEntry(
    ...     msgid = '(DEPRECATED)',
    ...     msgstr= '(DETACERPED)')
    >>> deprecatedsetup([ped])
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something (DEPRECATED)')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something (DETACERPED)')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    msgstr inconsistently translated (DEPRECATED)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED, foo bar)',
    ...     msgstr= 'something (DETACERPED, foo bar)')
    >>> match(deprecated, pe)
    """
    if not ('(DEPRECATED)' in pe.msgstr or
            (deprecatedpe and
             deprecatedpe.msgstr in pe.msgstr)):
        yield "msgstr inconsistently translated (DEPRECATED)"

####################

def warningchecker(msgidpat=None):
    return levelchecker('warning', msgidpat)

@warningchecker()
def taildoublecolons(pe):
    """Check equality of tail '::'-ness between msgid and msgstr

    >>> pe = polib.POEntry(
    ...     msgid ='ends with ::',
    ...     msgstr='ends with ::')
    >>> for e in taildoublecolons(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid ='ends with ::',
    ...     msgstr='ends without double-colons')
    >>> for e in taildoublecolons(pe): print(e)
    tail '::'-ness differs between msgid and msgstr
    >>> pe = polib.POEntry(
    ...     msgid ='ends without double-colons',
    ...     msgstr='ends with ::')
    >>> for e in taildoublecolons(pe): print(e)
    tail '::'-ness differs between msgid and msgstr
    """
    if pe.msgid.endswith('::') != pe.msgstr.endswith('::'):
        yield "tail '::'-ness differs between msgid and msgstr"

@warningchecker()
def indentation(pe):
    """Check equality of initial indentation between msgid and msgstr

    This may report unexpected warning, because this doesn't aware
    the syntax of rst document and the context of msgstr.

    >>> pe = polib.POEntry(
    ...     msgid ='    indented text',
    ...     msgstr='  narrowed indentation')
    >>> for e in indentation(pe): print(e)
    initial indentation width differs betweeen msgid and msgstr
    """
    idindent = len(pe.msgid) - len(pe.msgid.lstrip())
    strindent = len(pe.msgstr) - len(pe.msgstr.lstrip())
    if idindent != strindent:
        yield "initial indentation width differs betweeen msgid and msgstr"

####################

def check(pofile, fatal=True, warning=False):
    targetlevel = { 'fatal': fatal, 'warning': warning }
    targetcheckers = [(checker, level)
                      for checker, level in checkers
                      if targetlevel[level]]
    if not targetcheckers:
        return []

    detected = []
    for checker in scanners:
        checker(pofile)
    for pe in pofile.translated_entries():
        errors = []
        for checker, level in targetcheckers:
            if match(checker, pe):
                errors.extend((level, checker.__name__, error)
                              for error in checker(pe))
        if errors:
            detected.append((pe, errors))
    return detected

########################################

if __name__ == "__main__":
    import sys
    import optparse

    optparser = optparse.OptionParser("""%prog [options] pofile ...

This checks Mercurial specific translation problems in specified
'*.po' files.

Each detected problems are shown in the format below::

    filename:linenum:type(checker): problem detail .....

"type" is "fatal" or "warning". "checker" is the name of the function
detecting corresponded error.

Checking by checker "foo" on the specific msgstr can be suppressed by
the "translator comment" like below. Multiple "no-xxxx-check" should
be separated by whitespaces::

    # no-foo-check
    msgid = "....."
    msgstr = "....."
""")
    optparser.add_option("", "--warning",
                         help="show also warning level problems",
                         action="store_true")
    optparser.add_option("", "--doctest",
                         help="run doctest of this tool, instead of check",
                         action="store_true")
    (options, args) = optparser.parse_args()

    if options.doctest:
        import os
        if 'TERM' in os.environ:
            del os.environ['TERM']
        import doctest
        failures, tests = doctest.testmod()
        sys.exit(failures and 1 or 0)

    # replace polib._POFileParser to show linenum of problematic msgstr
    class ExtPOFileParser(polib._POFileParser):
        def process(self, symbol, linenum):
            super(ExtPOFileParser, self).process(symbol, linenum)
            if symbol == 'MS': # msgstr
                self.current_entry.linenum = linenum
    polib._POFileParser = ExtPOFileParser

    detected = []
    warning = options.warning
    for f in args:
        detected.extend((f, pe, errors)
                        for pe, errors in check(polib.pofile(f),
                                                warning=warning))
    if detected:
        for f, pe, errors in detected:
            for level, checker, error in errors:
                sys.stderr.write('%s:%d:%s(%s): %s\n'
                                 % (f, pe.linenum, level, checker, error))
        sys.exit(1)