<filename>schedsi/log/binarylog.py
#!/usr/bin/env python3
"""Defines the :class:`BinaryLog` and the :func:`replay` function."""

import collections
import enum
import msgpack
from schedsi.cpu.time import Time, TimeType

_EntryType = enum.Enum('_EntryType', ['event', 'thread_statistics', 'cpu_statistics'])
_Event = enum.Enum('_Event', [
    'init_core',
    'context_switch',
    'thread_execute',
    'thread_yield',
    'cpu_idle',
    'timer_interrupt'
])

_GenericEvent = collections.namedtuple('_GenericEvent', 'cpu event')


def _encode_time(frac):
    """Encode :class:`~schedsi.cpu.time.Time`."""
    return {'numerator': int(frac.numerator), 'denominator': int(frac.denominator)}

def _encode_cpu(cpu):
    """Encode a :class:`~schedsi.cpu.Core` to a :obj:`dict`."""
    return {
        'uid': cpu.uid,
        'status': {'current_time': _encode_time(cpu.status.current_time)}
    }


def _encode_contexts(contexts, current_context):
    """Encode a :class:`~schedsi.context.Context` to a :obj:`dict`.

    `current_context` is the top context of the current
    :class:`context.chain <schedsi.context.Chain>.
    """
    def stringify_relationship(top, bottom):
        """Stringify the relationship between the :class:`Modules <schedsi.module.Module>` \
        `top` and `bottom`.

        `top` and `bottom` should refer to :class:`Modules <schedsi.module.Module>`
        following each other in the :class:`context.Chain <schedsi.context.Chain>`.
        """
        is_child = top.module.parent == bottom.module
        if not is_child:
            assert top.module == bottom.module
        return 'child' if is_child else 'sibling'

    first = contexts[0].thread
    chain = [{'thread': _encode_thread(first)}]
    if current_context is not None:
        chain[0].update({'relationship': stringify_relationship(first, current_context.thread)})

    prev = first
    for cur in (c.thread for c in contexts[1:]):
        chain.append({
            'thread': _encode_thread(cur),
            'relationship': stringify_relationship(cur, prev)
        })
        prev = cur

    return chain


def _encode_module(module):
    """Encode a :class:`~schedsi.module.Module` to a :obj:`dict`."""
    return {'name': module.name}


def _encode_thread(thread):
    """Encode a :class:`~schedsi.threads.Thread` to a :obj:`dict`."""
    return {'module': _encode_module(thread.module), 'tid': thread.tid}


def _encode_event(cpu, event, args=None):
    """Create a :class:`_GenericEvent`.

    `args` can contain additional parameters to put in the :obj:`dict`.
    """
    encoded = {'cpu': _encode_cpu(cpu), 'event': event, 'type': _EntryType.event.name}
    if args is not None:
        encoded.update(args)
    return encoded


def _encode_ctxsw(cpu, split_index, appendix, time):
    """Encode a context switching event to a :obj:`dict`."""
    if appendix is None:
        assert split_index is not None
        param = {'split_index': split_index}
    else:
        assert split_index is None
        param = {'appendix': _encode_contexts(appendix.contexts, cpu.status.chain.current_context)}

    param['time'] = _encode_time(time)

    return _encode_event(cpu, _Event.context_switch.name, param)


def _encode_coreinit(cpu):
    """Encode a init_core event to a :obj:`dict`."""
    return _encode_event(cpu, _Event.init_core.name,
                         {'context': _encode_contexts(cpu.status.chain.contexts, None)})


def _encode_time_sequence(times):
    """Encode a (potentially nested) sequence of `Time`s."""
    if isinstance(times, collections.Sequence):
        return times.__class__(map(_encode_time_sequence, times))
    return _encode_time(times)


def _encode_stats(stats):
    """Encode stats."""
    for key, value in stats.items():
        if isinstance(value, dict):
            stats[key] = _encode_stats(value)
        elif isinstance(value, collections.Sequence):
            stats[key] = _encode_time_sequence(value)
        elif isinstance(value, TimeType):
            stats[key] = _encode_time(value)
    return stats


class BinaryLog:
    """Binary logger using MessagePack."""

    def __init__(self, stream):
        """Create a :class:`BinaryLog`."""
        self.stream = stream
        self.packer = msgpack.Packer()

    def _write(self, data):
        """Write data to the MessagePack file."""
        self.stream.write(self.packer.pack(data))

    def _encode(self, cpu, event, args=None):
        """Encode an event and write data to the MessagePack file.

        See :func:`_encode_event`.
        """
        self._write(_encode_event(cpu, event.name, args))

    def init_core(self, cpu):
        """Register a :class:`Core`."""
        self._write(_encode_coreinit(cpu))

    def context_switch(self, cpu, split_index, appendix, time):
        """Log an context switch event."""
        self._write(_encode_ctxsw(cpu, split_index, appendix, time))

    def thread_execute(self, cpu, runtime):
        """Log an thread execution event."""
        self._encode(cpu, _Event.thread_execute, {'runtime': _encode_time(runtime)})

    def thread_yield(self, cpu):
        """Log an thread yielded event."""
        self._encode(cpu, _Event.thread_yield)

    def cpu_idle(self, cpu, idle_time):
        """Log an CPU idle event."""
        self._encode(cpu, _Event.cpu_idle, {'idle_time': _encode_time(idle_time)})

    def timer_interrupt(self, cpu, idx, delay):
        """Log an timer interrupt event."""
        self._encode(cpu, _Event.timer_interrupt, {'idx': idx, 'delay': _encode_time(delay)})

    def thread_statistics(self, stats):
        """Log thread statistics."""
        self._write({'type': _EntryType.thread_statistics.name, 'stats': _encode_stats(stats)})

    def cpu_statistics(self, stats):
        """Log CPU statistics."""
        self._write({'type': _EntryType.cpu_statistics.name,
                     'stats': list(map(_encode_stats, stats))})


# types emulating schedsi classes for other logs
_CPUContext = collections.namedtuple('_CPUContext', 'thread')
_Core = collections.namedtuple('_Core', 'uid status')

# namedtuples are immutable, but the following classes require mutable fields


class _Module:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.module.Module` emulation class."""

    def __init__(self, name):
        """Create a :class:`_Module`."""
        self.name = name
        self.parent = None


class _ContextChain:
    """A :class:`context.Chain <schedsi.context.Chain>` emulation class."""

    def __init__(self, contexts=None):
        """Create a :class:`_ContextChain`."""
        self.contexts = contexts or []

    def __len__(self):
        """Return the length of the :class:`_ContextChain`.

        See :meth:`context.Chain.__len__ <schedsi.context.Chain.__len__>`.
        """
        return len(self.contexts)

    @property
    def bottom(self):
        """The bottom thread.

        See :py:attr:`context.Chain.thread_at <schedsi.context.Chain.bottom>`.
        """
        return self.contexts[0].thread

    @property
    def top(self):
        """The top thread.

        See :py:attr:`context.Chain.top <schedsi.context.Chain.top>`.
        """
        return self.contexts[-1].thread

    @property
    def current_context(self):
        """The current (top) context.

        See :py:attr:`context.Chain.current_context <schedsi.context.Chain.current_context>`.
        """
        return self.contexts[-1]

    def thread_at(self, idx):
        """Return the thread at index `idx` in the chain.

        Negative values are treated as an offset from the back.

        See :meth:`context.Chain.thread_at <schedsi.context.Chain.thread_at>`.
        """
        return self.contexts[idx].thread


class _CPUStatus:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.cpu._Status` emulation class."""

    def __init__(self, current_time):
        """Create a :class:`_CPUStatus`."""
        self.current_time = current_time
        self.chain = _ContextChain()


class _Thread:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.threads.Thread` emulation class."""

    def __init__(self, tid, module):
        """Create a :class:`_Thread`."""
        self.tid = str(tid)
        self.module = module


def _decode_contexts(entries, current_context):
    """Extract :class:`_CPUContexts <_CPUContext>` from a :obj:`dict`-entry.

    `current_context` is the top context of the current
    :class:`context.chain <schedsi.context.Chain>.
    """
    contexts = [_CPUContext(_decode_thread(entry['thread'])) for entry in entries]
    if current_context is None:
        assert 'relationship' not in entries[0]
        return contexts
    for prev, cur, ent in zip([current_context] + contexts, contexts, entries):
        rel = ent['relationship']
        if rel == 'child':
            cur.thread.module.parent = prev.thread.module
        elif rel == 'sibling':
            cur.thread.module = prev.thread.module
        else:
            assert False, 'Invalid relationship: ' + rel
    return contexts


def _decode_generic_event(entry):
    """Convert a :obj:`dict-entry` to a :class:`_GenericEvent`.

    Returns :obj:`None` on failure.
    """
    if entry['type'] == _EntryType.event.name:
        return _GenericEvent(_decode_core(entry), entry['event'])
    return None


def _decode_core(entry):
    """Extract a :class:`_Core` from a :obj:`dict`-entry."""
    core = entry['cpu']
    return _Core(core['uid'], _decode_status(core['status']))


def _decode_time(entry):
    """Decode :class:`~schedsi.cpu.time.Time` from a :obj:`dict`-entry."""
    return Time(entry['numerator'], entry['denominator'])


def _decode_status(entry):
    """Extract :class:`_CPUStatus` from a :obj:`dict`-entry."""
    return _CPUStatus(_decode_time(entry['current_time']))


def _decode_thread(entry):
    """Extract a :class:`_Thread` from a :obj:`dict`-entry.

    Returns :obj:`None` if `entry` is :obj:`None`.
    """
    if not entry:
        return None
    return _Thread(entry['tid'], _decode_module(entry['module']))


def _decode_module(entry):
    """Extract a :class:`_Module` from a :obj:`dict`-entry.

    Returns :obj:`None` if `entry` is :obj:`None`.
    """
    if not entry:
        return None
    return _Module(entry['name'])


def _decode_ctxsw(cpu, entry):
    """Extract context switch arguments from a :obj:`dict`-entry.

    Returns a tuple (split_index, appendix) corresponding to the
    `split_index` and `appendix` parameters to :meth:`BinaryLog.context_switch`.
    """
    split_index = entry.get('split_index', None)
    appendix = entry.get('appendix', None)

    if split_index is not None:
        assert appendix is None
    else:
        assert appendix is not None
        appendix = _ContextChain(_decode_contexts(appendix, cpu.status.chain.current_context))

    return (split_index, appendix)


class TimeDecodeFail(ValueError):
    pass


def _try_decode_time(entry):
    """Try to decode `entry` as `Time`.

    Return None if unsuccessful.
    """
    try:
        return _decode_time(entry)
    except (KeyError, TypeError):
        raise TimeDecodeFail()


def _decode_time_sequence(times):
    """Decode a (potentially nested) sequence of `Time`s."""
    if isinstance(times, dict):
        return _decode_time(times)
    return times.__class__(map(_decode_time_sequence, times))


def _decode_stats(stats):
    """Decode stats."""
    for key, value in stats.items():
        if isinstance(value, collections.Sequence):
            stats[key] = _decode_time_sequence(value)
        elif isinstance(value, dict):
            try:
                stats[key] = _try_decode_time(value)
            except TimeDecodeFail:
                stats[key] = _decode_stats(value)
    return stats


def replay(binary, log):
    """Play a MessagePack file to another log."""
    contexts = {}
    for entry in msgpack.Unpacker(binary, read_size=16 * 1024, encoding='utf-8', use_list=False):
        event = _decode_generic_event(entry)
        if event is not None:
            if event.event == _Event.init_core.name:
                if event.cpu.uid in contexts:
                    raise RuntimeError('init_core found twice for same core')
                contexts[event.cpu.uid] = _decode_contexts(entry['context'], None)

            event.cpu.status.chain.contexts = contexts[event.cpu.uid]

            if event.event == _Event.init_core.name:
                log.init_core(event.cpu)
            elif event.event == _Event.context_switch.name:
                split_index, appendix = _decode_ctxsw(event.cpu, entry)
                log.context_switch(event.cpu, split_index, appendix, _decode_time(entry['time']))
                chain = event.cpu.status.chain
                if split_index is not None:
                    del chain.contexts[split_index + 1:]
                else:
                    chain.contexts += appendix.contexts
            elif event.event == _Event.thread_execute.name:
                log.thread_execute(event.cpu, _decode_time(entry['runtime']))
            elif event.event == _Event.thread_yield.name:
                log.thread_yield(event.cpu)
            elif event.event == _Event.cpu_idle.name:
                log.cpu_idle(event.cpu, _decode_time(entry['idle_time']))
            elif event.event == _Event.timer_interrupt.name:
                log.timer_interrupt(event.cpu, entry['idx'], _decode_time(entry['delay']))
            else:
                print('Unknown event:', event)
        elif entry['type'] == _EntryType.thread_statistics.name:
            log.thread_statistics(_decode_stats(entry['stats']))
        elif entry['type'] == _EntryType.cpu_statistics.name:
            log.cpu_statistics(map(_decode_stats, entry['stats']))
        else:
            print('Unknown entry:', entry)


def get_thread_statistics(binary):
    """Read thread statistics from a MessagePack file."""
    for entry in msgpack.Unpacker(binary, read_size=16 * 1024, encoding='utf-8', use_list=False):
        if entry['type'] == _EntryType.thread_statistics.name:
            return entry['stats']

<filename>schedsi/log/binarylog.py
#!/usr/bin/env python3
"""Defines the :class:`BinaryLog` and the :func:`replay` function."""

import collections
import enum
import msgpack
from schedsi.cpu.time import Time, TimeType

_EntryType = enum.Enum('_EntryType', ['event', 'thread_statistics', 'cpu_statistics'])
_Event = enum.Enum('_Event', [
    'init_core',
    'context_switch',
    'thread_execute',
    'thread_yield',
    'cpu_idle',
    'timer_interrupt'
])

_GenericEvent = collections.namedtuple('_GenericEvent', 'cpu event')


def _encode_time(frac):
    """Encode :class:`~schedsi.cpu.time.Time`."""
    return {'numerator': int(frac.numerator), 'denominator': int(frac.denominator)}

def _encode_cpu(cpu):
    """Encode a :class:`~schedsi.cpu.Core` to a :obj:`dict`."""
    return {
        'uid': cpu.uid,
        'status': {'current_time': _encode_time(cpu.status.current_time)}
    }


def _encode_contexts(contexts, current_context):
    """Encode a :class:`~schedsi.context.Context` to a :obj:`dict`.

    `current_context` is the top context of the current
    :class:`context.chain <schedsi.context.Chain>.
    """
    def stringify_relationship(top, bottom):
        """Stringify the relationship between the :class:`Modules <schedsi.module.Module>` \
        `top` and `bottom`.

        `top` and `bottom` should refer to :class:`Modules <schedsi.module.Module>`
        following each other in the :class:`context.Chain <schedsi.context.Chain>`.
        """
        is_child = top.module.parent == bottom.module
        if not is_child:
            assert top.module == bottom.module
        return 'child' if is_child else 'sibling'

    first = contexts[0].thread
    chain = [{'thread': _encode_thread(first)}]
    if current_context is not None:
        chain[0].update({'relationship': stringify_relationship(first, current_context.thread)})

    prev = first
    for cur in (c.thread for c in contexts[1:]):
        chain.append({
            'thread': _encode_thread(cur),
            'relationship': stringify_relationship(cur, prev)
        })
        prev = cur

    return chain


def _encode_module(module):
    """Encode a :class:`~schedsi.module.Module` to a :obj:`dict`."""
    return {'name': module.name}


def _encode_thread(thread):
    """Encode a :class:`~schedsi.threads.Thread` to a :obj:`dict`."""
    return {'module': _encode_module(thread.module), 'tid': thread.tid}


def _encode_event(cpu, event, args=None):
    """Create a :class:`_GenericEvent`.

    `args` can contain additional parameters to put in the :obj:`dict`.
    """
    encoded = {'cpu': _encode_cpu(cpu), 'event': event, 'type': _EntryType.event.name}
    if args is not None:
        encoded.update(args)
    return encoded


def _encode_ctxsw(cpu, split_index, appendix, time):
    """Encode a context switching event to a :obj:`dict`."""
    if appendix is None:
        assert split_index is not None
        param = {'split_index': split_index}
    else:
        assert split_index is None
        param = {'appendix': _encode_contexts(appendix.contexts, cpu.status.chain.current_context)}

    param['time'] = _encode_time(time)

    return _encode_event(cpu, _Event.context_switch.name, param)


def _encode_coreinit(cpu):
    """Encode a init_core event to a :obj:`dict`."""
    return _encode_event(cpu, _Event.init_core.name,
                         {'context': _encode_contexts(cpu.status.chain.contexts, None)})


def _encode_time_sequence(times):
    """Encode a (potentially nested) sequence of `Time`s."""
    if isinstance(times, collections.Sequence):
        return times.__class__(map(_encode_time_sequence, times))
    return _encode_time(times)


def _encode_stats(stats):
    """Encode stats."""
    for key, value in stats.items():
        if isinstance(value, dict):
            stats[key] = _encode_stats(value)
        elif isinstance(value, collections.Sequence):
            stats[key] = _encode_time_sequence(value)
        elif isinstance(value, TimeType):
            stats[key] = _encode_time(value)
    return stats


class BinaryLog:
    """Binary logger using MessagePack."""

    def __init__(self, stream):
        """Create a :class:`BinaryLog`."""
        self.stream = stream
        self.packer = msgpack.Packer()

    def _write(self, data):
        """Write data to the MessagePack file."""
        self.stream.write(self.packer.pack(data))

    def _encode(self, cpu, event, args=None):
        """Encode an event and write data to the MessagePack file.

        See :func:`_encode_event`.
        """
        self._write(_encode_event(cpu, event.name, args))

    def init_core(self, cpu):
        """Register a :class:`Core`."""
        self._write(_encode_coreinit(cpu))

    def context_switch(self, cpu, split_index, appendix, time):
        """Log an context switch event."""
        self._write(_encode_ctxsw(cpu, split_index, appendix, time))

    def thread_execute(self, cpu, runtime):
        """Log an thread execution event."""
        self._encode(cpu, _Event.thread_execute, {'runtime': _encode_time(runtime)})

    def thread_yield(self, cpu):
        """Log an thread yielded event."""
        self._encode(cpu, _Event.thread_yield)

    def cpu_idle(self, cpu, idle_time):
        """Log an CPU idle event."""
        self._encode(cpu, _Event.cpu_idle, {'idle_time': _encode_time(idle_time)})

    def timer_interrupt(self, cpu, idx, delay):
        """Log an timer interrupt event."""
        self._encode(cpu, _Event.timer_interrupt, {'idx': idx, 'delay': _encode_time(delay)})

    def thread_statistics(self, stats):
        """Log thread statistics."""
        self._write({'type': _EntryType.thread_statistics.name, 'stats': _encode_stats(stats)})

    def cpu_statistics(self, stats):
        """Log CPU statistics."""
        self._write({'type': _EntryType.cpu_statistics.name,
                     'stats': list(map(_encode_stats, stats))})


# types emulating schedsi classes for other logs
_CPUContext = collections.namedtuple('_CPUContext', 'thread')
_Core = collections.namedtuple('_Core', 'uid status')

# namedtuples are immutable, but the following classes require mutable fields


class _Module:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.module.Module` emulation class."""

    def __init__(self, name):
        """Create a :class:`_Module`."""
        self.name = name
        self.parent = None


class _ContextChain:
    """A :class:`context.Chain <schedsi.context.Chain>` emulation class."""

    def __init__(self, contexts=None):
        """Create a :class:`_ContextChain`."""
        self.contexts = contexts or []

    def __len__(self):
        """Return the length of the :class:`_ContextChain`.

        See :meth:`context.Chain.__len__ <schedsi.context.Chain.__len__>`.
        """
        return len(self.contexts)

    @property
    def bottom(self):
        """The bottom thread.

        See :py:attr:`context.Chain.thread_at <schedsi.context.Chain.bottom>`.
        """
        return self.contexts[0].thread

    @property
    def top(self):
        """The top thread.

        See :py:attr:`context.Chain.top <schedsi.context.Chain.top>`.
        """
        return self.contexts[-1].thread

    @property
    def current_context(self):
        """The current (top) context.

        See :py:attr:`context.Chain.current_context <schedsi.context.Chain.current_context>`.
        """
        return self.contexts[-1]

    def thread_at(self, idx):
        """Return the thread at index `idx` in the chain.

        Negative values are treated as an offset from the back.

        See :meth:`context.Chain.thread_at <schedsi.context.Chain.thread_at>`.
        """
        return self.contexts[idx].thread


class _CPUStatus:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.cpu._Status` emulation class."""

    def __init__(self, current_time):
        """Create a :class:`_CPUStatus`."""
        self.current_time = current_time
        self.chain = _ContextChain()


class _Thread:  # pylint: disable=too-few-public-methods
    """A :class:`~schedsi.threads.Thread` emulation class."""

    def __init__(self, tid, module):
        """Create a :class:`_Thread`."""
        self.tid = str(tid)
        self.module = module


def _decode_contexts(entries, current_context):
    """Extract :class:`_CPUContexts <_CPUContext>` from a :obj:`dict`-entry.

    `current_context` is the top context of the current
    :class:`context.chain <schedsi.context.Chain>.
    """
    contexts = [_CPUContext(_decode_thread(entry['thread'])) for entry in entries]
    if current_context is None:
        assert 'relationship' not in entries[0]
        return contexts
    for prev, cur, ent in zip([current_context] + contexts, contexts, entries):
        rel = ent['relationship']
        if rel == 'child':
            cur.thread.module.parent = prev.thread.module
        elif rel == 'sibling':
            cur.thread.module = prev.thread.module
        else:
            assert False, 'Invalid relationship: ' + rel
    return contexts


def _decode_generic_event(entry):
    """Convert a :obj:`dict-entry` to a :class:`_GenericEvent`.

    Returns :obj:`None` on failure.
    """
    if entry['type'] == _EntryType.event.name:
        return _GenericEvent(_decode_core(entry), entry['event'])
    return None


def _decode_core(entry):
    """Extract a :class:`_Core` from a :obj:`dict`-entry."""
    core = entry['cpu']
    return _Core(core['uid'], _decode_status(core['status']))


def _decode_time(entry):
    """Decode :class:`~schedsi.cpu.time.Time` from a :obj:`dict`-entry."""
    return Time(entry['numerator'], entry['denominator'])


def _decode_status(entry):
    """Extract :class:`_CPUStatus` from a :obj:`dict`-entry."""
    return _CPUStatus(_decode_time(entry['current_time']))


def _decode_thread(entry):
    """Extract a :class:`_Thread` from a :obj:`dict`-entry.

    Returns :obj:`None` if `entry` is :obj:`None`.
    """
    if not entry:
        return None
    return _Thread(entry['tid'], _decode_module(entry['module']))


def _decode_module(entry):
    """Extract a :class:`_Module` from a :obj:`dict`-entry.

    Returns :obj:`None` if `entry` is :obj:`None`.
    """
    if not entry:
        return None
    return _Module(entry['name'])


def _decode_ctxsw(cpu, entry):
    """Extract context switch arguments from a :obj:`dict`-entry.

    Returns a tuple (split_index, appendix) corresponding to the
    `split_index` and `appendix` parameters to :meth:`BinaryLog.context_switch`.
    """
    split_index = entry.get('split_index', None)
    appendix = entry.get('appendix', None)

    if split_index is not None:
        assert appendix is None
    else:
        assert appendix is not None
        appendix = _ContextChain(_decode_contexts(appendix, cpu.status.chain.current_context))

    return (split_index, appendix)


class TimeDecodeFail(ValueError):
    pass


def _try_decode_time(entry):
    """Try to decode `entry` as `Time`.

    Return None if unsuccessful.
    """
    try:
        return _decode_time(entry)
    except (KeyError, TypeError):
        raise TimeDecodeFail()


def _decode_time_sequence(times):
    """Decode a (potentially nested) sequence of `Time`s."""
    if isinstance(times, dict):
        return _decode_time(times)
    return times.__class__(map(_decode_time_sequence, times))


def _decode_stats(stats):
    """Decode stats."""
    for key, value in stats.items():
        if isinstance(value, collections.Sequence):
            stats[key] = _decode_time_sequence(value)
        elif isinstance(value, dict):
            try:
                stats[key] = _try_decode_time(value)
            except TimeDecodeFail:
                stats[key] = _decode_stats(value)
    return stats


def replay(binary, log):
    """Play a MessagePack file to another log."""
    contexts = {}
    for entry in msgpack.Unpacker(binary, read_size=16 * 1024, encoding='utf-8', use_list=False):
        event = _decode_generic_event(entry)
        if event is not None:
            if event.event == _Event.init_core.name:
                if event.cpu.uid in contexts:
                    raise RuntimeError('init_core found twice for same core')
                contexts[event.cpu.uid] = _decode_contexts(entry['context'], None)

            event.cpu.status.chain.contexts = contexts[event.cpu.uid]

            if event.event == _Event.init_core.name:
                log.init_core(event.cpu)
            elif event.event == _Event.context_switch.name:
                split_index, appendix = _decode_ctxsw(event.cpu, entry)
                log.context_switch(event.cpu, split_index, appendix, _decode_time(entry['time']))
                chain = event.cpu.status.chain
                if split_index is not None:
                    del chain.contexts[split_index + 1:]
                else:
                    chain.contexts += appendix.contexts
            elif event.event == _Event.thread_execute.name:
                log.thread_execute(event.cpu, _decode_time(entry['runtime']))
            elif event.event == _Event.thread_yield.name:
                log.thread_yield(event.cpu)
            elif event.event == _Event.cpu_idle.name:
                log.cpu_idle(event.cpu, _decode_time(entry['idle_time']))
            elif event.event == _Event.timer_interrupt.name:
                log.timer_interrupt(event.cpu, entry['idx'], _decode_time(entry['delay']))
            else:
                print('Unknown event:', event)
        elif entry['type'] == _EntryType.thread_statistics.name:
            log.thread_statistics(_decode_stats(entry['stats']))
        elif entry['type'] == _EntryType.cpu_statistics.name:
            log.cpu_statistics(map(_decode_stats, entry['stats']))
        else:
            print('Unknown entry:', entry)


def get_thread_statistics(binary):
    """Read thread statistics from a MessagePack file."""
    for entry in msgpack.Unpacker(binary, read_size=16 * 1024, encoding='utf-8', use_list=False):
        if entry['type'] == _EntryType.thread_statistics.name:
            return entry['stats']

#!/usr/bin/env python3 Defines the :class:`BinaryLog` and the :func:`replay` function. Encode :class:`~schedsi.cpu.time.Time`. Encode a :class:`~schedsi.cpu.Core` to a :obj:`dict`. Encode a :class:`~schedsi.context.Context` to a :obj:`dict`.      `current_context` is the top context of the current     :class:`context.chain <schedsi.context.Chain>. Stringify the relationship between the :class:`Modules <schedsi.module.Module>` \         `top` and `bottom`.          `top` and `bottom` should refer to :class:`Modules <schedsi.module.Module>`         following each other in the :class:`context.Chain <schedsi.context.Chain>`. Encode a :class:`~schedsi.module.Module` to a :obj:`dict`. Encode a :class:`~schedsi.threads.Thread` to a :obj:`dict`. Create a :class:`_GenericEvent`.      `args` can contain additional parameters to put in the :obj:`dict`. Encode a context switching event to a :obj:`dict`. Encode a init_core event to a :obj:`dict`. Encode a (potentially nested) sequence of `Time`s. Encode stats. Binary logger using MessagePack. Create a :class:`BinaryLog`. Write data to the MessagePack file. Encode an event and write data to the MessagePack file.          See :func:`_encode_event`. Register a :class:`Core`. Log an context switch event. Log an thread execution event. Log an thread yielded event. Log an CPU idle event. Log an timer interrupt event. Log thread statistics. Log CPU statistics. # types emulating schedsi classes for other logs # namedtuples are immutable, but the following classes require mutable fields # pylint: disable=too-few-public-methods A :class:`~schedsi.module.Module` emulation class. Create a :class:`_Module`. A :class:`context.Chain <schedsi.context.Chain>` emulation class. Create a :class:`_ContextChain`. Return the length of the :class:`_ContextChain`.          See :meth:`context.Chain.__len__ <schedsi.context.Chain.__len__>`. The bottom thread.          See :py:attr:`context.Chain.thread_at <schedsi.context.Chain.bottom>`. The top thread.          See :py:attr:`context.Chain.top <schedsi.context.Chain.top>`. The current (top) context.          See :py:attr:`context.Chain.current_context <schedsi.context.Chain.current_context>`. Return the thread at index `idx` in the chain.          Negative values are treated as an offset from the back.          See :meth:`context.Chain.thread_at <schedsi.context.Chain.thread_at>`. # pylint: disable=too-few-public-methods A :class:`~schedsi.cpu._Status` emulation class. Create a :class:`_CPUStatus`. # pylint: disable=too-few-public-methods A :class:`~schedsi.threads.Thread` emulation class. Create a :class:`_Thread`. Extract :class:`_CPUContexts <_CPUContext>` from a :obj:`dict`-entry.      `current_context` is the top context of the current     :class:`context.chain <schedsi.context.Chain>. Convert a :obj:`dict-entry` to a :class:`_GenericEvent`.      Returns :obj:`None` on failure. Extract a :class:`_Core` from a :obj:`dict`-entry. Decode :class:`~schedsi.cpu.time.Time` from a :obj:`dict`-entry. Extract :class:`_CPUStatus` from a :obj:`dict`-entry. Extract a :class:`_Thread` from a :obj:`dict`-entry.      Returns :obj:`None` if `entry` is :obj:`None`. Extract a :class:`_Module` from a :obj:`dict`-entry.      Returns :obj:`None` if `entry` is :obj:`None`. Extract context switch arguments from a :obj:`dict`-entry.      Returns a tuple (split_index, appendix) corresponding to the     `split_index` and `appendix` parameters to :meth:`BinaryLog.context_switch`. Try to decode `entry` as `Time`.      Return None if unsuccessful. Decode a (potentially nested) sequence of `Time`s. Decode stats. Play a MessagePack file to another log. Read thread statistics from a MessagePack file.