def _trace_dispatch(frame, event, arg):
    # type: (Any, str, Optional[Any]) -> None
    """
    This is the main hook passed to setprofile().
    It implement python profiler interface.

    Arguments are described in https://docs.python.org/2/library/sys.html#sys.settrace
    """
    # Bail if we're not tracing.
    if not running:
        return

    # Get counter for this code object.  Bail if we don't care about this function.
    # An explicit None is stored in the table when we no longer care.
    code = frame.f_code
    key = id(code)
    n = sampling_counters.get(key, 0)
    if n is None:
        return

    if event == 'call':
        # Bump counter and bail depending on sampling sequence.
        sampling_counters[key] = n + 1
        # Each function gets traced at most MAX_SAMPLES_PER_FUNC times per run.
        # NOTE: There's a race condition if two threads call the same function.
        # I don't think we should care, so what if it gets probed an extra time.
        if n not in sampling_sequence:
            if n > LAST_SAMPLE:
                sampling_counters[key] = None  # We're no longer interested in this function.
            call_pending.discard(key)  # Avoid getting events out of sync
            return
        # Mark that we are looking for a return from this code object.
        call_pending.add(key)
    elif event == 'return':
        if key not in call_pending:
            # No pending call event -- ignore this event. We only collect
            # return events when we know the corresponding call event.
            return
        call_pending.discard(key)  # Avoid race conditions
    else:
        # Ignore other events, such as c_call and c_return.
        return

    # Track calls under current directory only.
    filename = _filter_filename(code.co_filename)
    if filename:
        func_name = get_function_name_from_frame(frame)
        if not func_name or func_name[0] == '<':
            # Could be a lambda or a comprehension; we're not interested.
            sampling_counters[key] = None
        else:
            function_key = FunctionKey(filename, code.co_firstlineno, func_name)
            if event == 'call':
                # TODO(guido): Make this faster
                arg_info = inspect.getargvalues(frame)  # type: ArgInfo
                resolved_types = prep_args(arg_info)
                _task_queue.put(KeyAndTypes(function_key, resolved_types))
            elif event == 'return':
                # This event is also triggered if a function yields or raises an exception.
                # We can tell the difference by looking at the bytecode.
                # (We don't get here for C functions so the bytecode always exists.)
                last_opcode = code.co_code[frame.f_lasti]
                if last_opcode == RETURN_VALUE_OPCODE:
                    if code.co_flags & CO_GENERATOR:
                        # Return from a generator.
                        t = resolve_type(FakeIterator([]))
                    else:
                        t = resolve_type(arg)
                elif last_opcode == YIELD_VALUE_OPCODE:
                    # Yield from a generator.
                    # TODO: Unify generators -- currently each YIELD is turned into
                    # a separate call, so a function yielding ints and strs will be
                    # typed as Union[Iterator[int], Iterator[str]] -- this should be
                    # Iterator[Union[int, str]].
                    t = resolve_type(FakeIterator([arg]))
                else:
                    # This branch is also taken when returning from a generator.
                    # TODO: returning non-trivial values from generators, per PEP 380;
                    # and async def / await stuff.
                    t = NoReturnType
                _task_queue.put(KeyAndReturn(function_key, t))
    else:
        sampling_counters[key] = None