async function evaluate(encode, prerun, postrun) {
    const df = this,
      async = [];

    // if the pulse value is set, this is a re-entrant call
    if (df._pulse) return reentrant(df);

    // wait for pending datasets to load
    if (df._pending) await df._pending;

    // invoke prerun function, if provided
    if (prerun) await asyncCallback(df, prerun);

    // exit early if there are no updates
    if (!df._touched.length) {
      df.debug('Dataflow invoked, but nothing to do.');
      return df;
    }

    // increment timestamp clock
    const stamp = ++df._clock;

    // set the current pulse
    df._pulse = new Pulse(df, stamp, encode);

    // initialize priority queue, reset touched operators
    df._touched.forEach(op => df._enqueue(op, true));
    df._touched = UniqueList(id);
    let count = 0,
      op,
      next,
      error;
    try {
      while (df._heap.size() > 0) {
        // dequeue operator with highest priority
        op = df._heap.pop();

        // re-queue if rank changed
        if (op.rank !== op.qrank) {
          df._enqueue(op, true);
          continue;
        }

        // otherwise, evaluate the operator
        next = op.run(df._getPulse(op, encode));
        if (next.then) {
          // await if operator returns a promise directly
          next = await next;
        } else if (next.async) {
          // queue parallel asynchronous execution
          async.push(next.async);
          next = StopPropagation;
        }

        // propagate evaluation, enqueue dependent operators
        if (next !== StopPropagation) {
          if (op._targets) op._targets.forEach(op => df._enqueue(op));
        }

        // increment visit counter
        ++count;
      }
    } catch (err) {
      df._heap.clear();
      error = err;
    }

    // reset pulse map
    df._input = {};
    df._pulse = null;
    df.debug(`Pulse ${stamp}: ${count} operators`);
    if (error) {
      df._postrun = [];
      df.error(error);
    }

    // invoke callbacks queued via runAfter
    if (df._postrun.length) {
      const pr = df._postrun.sort((a, b) => b.priority - a.priority);
      df._postrun = [];
      for (let i = 0; i < pr.length; ++i) {
        await asyncCallback(df, pr[i].callback);
      }
    }

    // invoke postrun function, if provided
    if (postrun) await asyncCallback(df, postrun);

    // handle non-blocking asynchronous callbacks
    if (async.length) {
      Promise.all(async).then(cb => df.runAsync(null, () => {
        cb.forEach(f => {
          try {
            f(df);
          } catch (err) {
            df.error(err);
          }
        });
      }));
    }
    return df;
  }
Evaluates the dataflow and returns a Promise that resolves when pulse
propagation completes. This method will increment the current timestamp
and process all updated, pulsed and touched operators. When invoked for
the first time, all registered operators will be processed. This method
should not be invoked by third-party clients, use {@link runAsync} or
{@link run} instead.
@param {string} [encode] - The name of an encoding set to invoke during
  propagation. This value is added to generated Pulse instances;
  operators can then respond to (or ignore) this setting as appropriate.
  This parameter can be used in conjunction with the Encode transform in
  the vega-encode package.
@param {function} [prerun] - An optional callback function to invoke
  immediately before dataflow evaluation commences.
@param {function} [postrun] - An optional callback function to invoke
  after dataflow evaluation completes. The callback will be invoked
  after those registered via {@link runAfter}.
@return {Promise} - A promise that resolves to this dataflow after
  evaluation completes.