contrib/pull_logger.py
author Pierre-Yves David <pierre-yves.david@octobus.net>
Sat, 12 Nov 2022 02:38:53 +0100
branchstable
changeset 49563 08fe5c4d4471
parent 49514 946c023212b8
permissions -rw-r--r--
tags-fnode-cache: skip building a changectx in getfnode Building a changectx object is costly, doing it just to retrieve the revision number is suboptimal. Directly fetching the revision number from the changelog provide a sizeable speedup to `hg debugupdatecache`. ### data-env-vars.name = mercurial-2018-08-01-zstd-sparse-revlog # benchmark.name = debug-update-cache # benchmark.variants.pre-state = warm before: 0.213229 seconds after: 0.165577 seconds (-22.35%) # data-env-vars.name = mercurial-filtered-2019-11-22-zstd-sparse-revlog before: 1.200383 seconds after: 1.071618 seconds (-10.73%) # data-env-vars.name = mozilla-central-2018-08-01-zstd-sparse-revlog before: 1.465735 seconds after: 0.923128 seconds (-37.02%) # data-env-vars.name = mozilla-try-2019-02-18-zstd-sparse-revlog before: 6.511771 seconds after: 4.507316 seconds (-30.78%) # data-env-vars.name = netbeans-2018-08-01-zstd-sparse-revlog before: 1.023007 seconds after: 0.645026 seconds (-36.95%) # data-env-vars.name = pypy-2018-08-01-zstd-sparse-revlog before: 0.381141 seconds after: 0.268654 seconds (-29.51%)

# pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS.
#
# Copyright 2022  Pacien TRAN-GIRARD <pacien.trangirard@pacien.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.


'''logs pull parameters to a file

This extension logs the pull parameters, i.e. the remote and common heads,
when pulling from the local repository.

The collected data should give an idea of the state of a pair of repositories
and allow replaying past synchronisations between them. This is particularly
useful for working on data exchange, bundling and caching-related
optimisations.

The record is a JSON-line file located in the repository's VFS at
.hg/pull_log.jsonl.

Log write failures are not considered fatal: log writes may be skipped for any
reason such as insufficient storage or a timeout.

Some basic log file rotation can be enabled by setting 'rotate-size' to a value
greater than 0. This causes the current log file to be moved to
.hg/pull_log.jsonl.rotated when this threshold is met, discarding any
previously rotated log file.

The timeouts of the exclusive lock used when writing to the lock file can be
configured through the 'timeout.lock' and 'timeout.warn' options of this
plugin. Those are not expected to be held for a significant time in practice.::

  [pull-logger]
  timeout.lock = 300
  timeout.warn = 100
  rotate-size = 1kb
'''


import json
import time

from mercurial.i18n import _
from mercurial.utils import stringutil
from mercurial import (
    error,
    extensions,
    lock,
    registrar,
    wireprotov1server,
)

EXT_NAME = b'pull-logger'
EXT_VERSION_CODE = 0

LOG_FILE = b'pull_log.jsonl'
OLD_LOG_FILE = LOG_FILE + b'.rotated'
LOCK_NAME = LOG_FILE + b'.lock'

configtable = {}
configitem = registrar.configitem(configtable)
configitem(EXT_NAME, b'timeout.lock', default=600)
configitem(EXT_NAME, b'timeout.warn', default=120)
configitem(EXT_NAME, b'rotate-size', default=b'100MB')


def wrap_getbundle(orig, repo, proto, others, *args, **kwargs):
    heads, common = extract_pull_heads(others)
    log_entry = {
        'timestamp': time.time(),
        'logger_version': EXT_VERSION_CODE,
        'heads': sorted(heads),
        'common': sorted(common),
    }

    try:
        write_to_log(repo, log_entry)
    except (IOError, error.LockError) as err:
        msg = stringutil.forcebytestr(err)
        repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg)

    return orig(repo, proto, others, *args, **kwargs)


def extract_pull_heads(bundle_args):
    opts = wireprotov1server.options(
        b'getbundle',
        wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
        bundle_args.copy(),  # this call consumes the args destructively
    )

    heads = opts.get(b'heads', b'').decode('utf-8').split(' ')
    common = opts.get(b'common', b'').decode('utf-8').split(' ')
    return (heads, common)


def write_to_log(repo, entry):
    locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock')
    lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn')
    rotatesize = repo.ui.configbytes(EXT_NAME, b'rotate-size')

    with lock.trylock(
        ui=repo.ui,
        vfs=repo.vfs,
        lockname=LOCK_NAME,
        timeout=locktimeout,
        warntimeout=lockwarntimeout,
    ):
        if rotatesize > 0 and repo.vfs.exists(LOG_FILE):
            if repo.vfs.stat(LOG_FILE).st_size >= rotatesize:
                repo.vfs.rename(LOG_FILE, OLD_LOG_FILE)

        with repo.vfs.open(LOG_FILE, b'a+') as logfile:
            serialised = json.dumps(entry, sort_keys=True)
            logfile.write(serialised.encode('utf-8'))
            logfile.write(b'\n')
            logfile.flush()


def reposetup(ui, repo):
    if repo.local():
        repo._wlockfreeprefix.add(LOG_FILE)
        repo._wlockfreeprefix.add(OLD_LOG_FILE)


def uisetup(ui):
    del wireprotov1server.commands[b'getbundle']
    decorator = wireprotov1server.wireprotocommand(
        name=b'getbundle',
        args=b'*',
        permission=b'pull',
    )

    extensions.wrapfunction(
        container=wireprotov1server,
        funcname='getbundle',
        wrapper=wrap_getbundle,
    )

    decorator(wireprotov1server.getbundle)