Skip to content

Latest commit

 

History

History
78 lines (62 loc) · 2.88 KB

File metadata and controls

78 lines (62 loc) · 2.88 KB

0003 - Operator interface and ExecutionContext

Exact signatures

constexpr size_t STANDARD_VECTOR_SIZE = 1024;

struct SelectionVector {
    uint32_t indices[STANDARD_VECTOR_SIZE];
    size_t count;
};

class Batch {
public:
    ColumnVector& column(size_t i);
    size_t num_columns() const;
    size_t count() const;                     // logical row count
    size_t base_count() const;                // physical rows filled
    const SelectionVector* selection() const; // nullptr = no filtering active
    size_t row_index(size_t i) const;         // logical -> physical
    void reset();
};

class Operator {
public:
    virtual void open(ExecutionContext& ctx) = 0;
    virtual bool next(ExecutionContext& ctx, Batch& out) = 0; // false = EOF
    virtual void close(ExecutionContext& ctx) = 0;
    virtual size_t num_output_columns() const = 0;
    virtual ~Operator() = default;
};

struct ExecutionContext {
    Profiler profiler;
    // memory accounting, cancellation, tracing live here later - not on Operator
};

num_output_columns() is the v0 stand-in for output schema information - the consumer needs it to size the root batch, and Project needs it to size its input batch. It grows into a real schema object when types beyond int64 arrive.

ExecutionContext responsibilities

ExecutionContext carries everything that is per-execution rather than per-plan: today that is only the profiler; later it is the memory accountant, the cancellation flag, and the tracer. Operators stay stateless with respect to these concerns - nothing cross-cutting lives on Operator, so adding a concern never touches operator code.

Profiler design

  • Operators register a stats slot in open() via ctx.profiler.get("Name") and cache the returned pointer. Registration happens child-first, so the report prints in pipeline order (Scan, Filter, Project). No string lookup ever happens inside next().
  • Per-slot stats: exclusive time (ns), calls, rows in, rows out.
  • ScopedTimer is RAII around the timed region. When profiler.enabled == false it compiles down to a null-pointer check - the benchmark's headline numbers come from unprofiled runs.

Timing semantics: exclusive, by construction

Each operator times only its own work, not its child's. This falls out of where the timer is placed rather than from subtraction at report time:

  • Scan::next times its whole body (it has no child).
  • Filter::next starts its timer after child->next() returns.
  • Project::next starts its timer after its input batch is filled.

So the per-operator table is exclusive time and the columns sum (approximately) to the query total.

Caveat: with batch_size = 1, a profiled run performs two clock reads per operator per tuple - tens of millions of clock calls. Profiled runs are for the shape of the operator table (rows in/out, relative cost); headline timings always come from unprofiled runs.