docs: 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