Skip to content

Commit 0aa09b5

Browse files
committed
tp: scan dataframes without SQLite
A query which only reads a dataframe gives SQLite nothing to do. Read columns from dataframe storage and return borrowed batch views instead. Columns without one slot per row are expanded into a reusable batch-sized buffer. The planner still sends filtered, joined, grouped, or computed queries through SQLite.
1 parent ecd799a commit 0aa09b5

6 files changed

Lines changed: 478 additions & 0 deletions

File tree

Android.bp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17667,6 +17667,7 @@ filegroup {
1766717667
name: "perfetto_src_trace_processor_core_exec_exec",
1766817668
srcs: [
1766917669
"src/trace_processor/core/exec/column_view.cc",
17670+
"src/trace_processor/core/exec/dataframe_scan.cc",
1767017671
"src/trace_processor/core/exec/operator.cc",
1767117672
"src/trace_processor/core/exec/pipeline.cc",
1767217673
"src/trace_processor/core/exec/row_batch.cc",
@@ -17684,6 +17685,7 @@ filegroup {
1768417685
filegroup {
1768517686
name: "perfetto_src_trace_processor_core_exec_unittests",
1768617687
srcs: [
17688+
"src/trace_processor/core/exec/dataframe_scan_unittest.cc",
1768717689
"src/trace_processor/core/exec/operator_unittest.cc",
1768817690
"src/trace_processor/core/exec/row_store_unittest.cc",
1768917691
],

src/trace_processor/core/dataframe/dataframe.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ class Dataframe {
276276
// Returns the column names of the dataframe.
277277
const std::vector<std::string>& column_names() const { return column_names_; }
278278

279+
// Returns `column`'s values and which rows hold one, for reading them
280+
// without going through a cursor.
281+
const Column& column(uint32_t column) const { return *column_ptrs_[column]; }
279282
// Returns the type of the values in `column`.
280283
StorageType column_type(uint32_t column) const {
281284
return column_ptrs_[column]->storage.type();

src/trace_processor/core/exec/BUILD.gn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ source_set("exec") {
1818
sources = [
1919
"column_view.cc",
2020
"column_view.h",
21+
"dataframe_scan.cc",
22+
"dataframe_scan.h",
2123
"operator.cc",
2224
"operator.h",
2325
"pipeline.cc",
@@ -36,6 +38,7 @@ source_set("exec") {
3638
"../../../base",
3739
"../../containers",
3840
"../common",
41+
"../dataframe",
3942
"../util",
4043
]
4144
}
@@ -49,6 +52,7 @@ source_set("test_utils") {
4952
perfetto_unittest_source_set("unittests") {
5053
testonly = true
5154
sources = [
55+
"dataframe_scan_unittest.cc",
5256
"operator_unittest.cc",
5357
"row_store_unittest.cc",
5458
]
@@ -60,6 +64,7 @@ perfetto_unittest_source_set("unittests") {
6064
"../../../base",
6165
"../../containers",
6266
"../common",
67+
"../dataframe",
6368
"../util",
6469
]
6570
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "src/trace_processor/core/exec/dataframe_scan.h"
18+
19+
#include <algorithm>
20+
#include <cstdint>
21+
#include <memory>
22+
#include <utility>
23+
#include <vector>
24+
25+
#include "src/trace_processor/containers/string_pool.h"
26+
#include "src/trace_processor/core/common/storage_types.h"
27+
#include "src/trace_processor/core/dataframe/dataframe.h"
28+
#include "src/trace_processor/core/dataframe/types.h"
29+
#include "src/trace_processor/core/exec/column_view.h"
30+
#include "src/trace_processor/core/exec/operator.h"
31+
#include "src/trace_processor/core/exec/row_batch.h"
32+
#include "src/trace_processor/core/exec/row_selection.h"
33+
#include "src/trace_processor/core/util/bit_vector.h"
34+
#include "src/trace_processor/core/util/flex_vector.h"
35+
36+
namespace perfetto::trace_processor::core::exec {
37+
// Lays a batch's worth of a column which does not store one value per row back
38+
// out so that it does. The buffer is a batch wide and reused, so a scan of a
39+
// sparse column costs one batch of work at a time rather than the whole column
40+
// up front.
41+
class DataframeScan::Expander {
42+
public:
43+
virtual ~Expander();
44+
45+
// Lays rows [from, from + count) out densely from zero and points `view` at
46+
// them. Called with successive ranges starting at row zero.
47+
virtual void Expand(uint32_t from, uint32_t count, ColumnView* view) = 0;
48+
49+
// Keeps the values alive for as long as a batch holds them.
50+
virtual std::shared_ptr<const void> owner() const = 0;
51+
52+
virtual void Rewind() = 0;
53+
};
54+
55+
DataframeScan::Expander::~Expander() = default;
56+
57+
namespace {
58+
59+
template <typename T>
60+
class ExpanderImpl final : public DataframeScan::Expander {
61+
public:
62+
ExpanderImpl(StorageType type, const T* packed, const BitVector* bits)
63+
: type_(type), packed_(packed), bits_(bits) {
64+
buffer_->values = FlexVector<T>::CreateWithSize(kMaxBatchRows);
65+
buffer_->validity = BitVector::CreateWithSize(kMaxBatchRows);
66+
}
67+
68+
void Expand(uint32_t from, uint32_t count, ColumnView* view) override {
69+
PERFETTO_DCHECK(from == next_);
70+
buffer_->validity.ClearAllBits();
71+
for (uint32_t row = 0; row < count; ++row) {
72+
if (bits_->is_set(from + row)) {
73+
buffer_->values[row] = packed_[consumed_++];
74+
buffer_->validity.set(row);
75+
} else {
76+
// Written even for a null row, so the storage is readable everywhere.
77+
buffer_->values[row] = T{};
78+
}
79+
}
80+
next_ = from + count;
81+
*view = ColumnView::Reference(type_, buffer_->values.data(),
82+
&buffer_->validity);
83+
}
84+
85+
std::shared_ptr<const void> owner() const override { return buffer_; }
86+
87+
void Rewind() override {
88+
consumed_ = 0;
89+
next_ = 0;
90+
}
91+
92+
private:
93+
struct Buffer {
94+
FlexVector<T> values;
95+
BitVector validity;
96+
};
97+
98+
StorageType type_;
99+
const T* packed_;
100+
const BitVector* bits_;
101+
std::shared_ptr<Buffer> buffer_ = std::make_shared<Buffer>();
102+
// How many of the packed values have been read, which is how many rows
103+
// before `next_` hold one.
104+
uint32_t consumed_ = 0;
105+
uint32_t next_ = 0;
106+
};
107+
108+
// Builds either a view straight onto the dataframe's storage or, for a column
109+
// without a slot per row, the expander which fills one batch of it.
110+
template <typename T>
111+
void BuildColumn(const dataframe::Column& column,
112+
StorageType type,
113+
ColumnView* view,
114+
std::shared_ptr<const void>* owner,
115+
std::unique_ptr<DataframeScan::Expander>* expander) {
116+
const T* data =
117+
column.storage
118+
.template unchecked_data<typename core::TypeTagFor<T>::type>();
119+
const auto& nulls = column.null_storage;
120+
if (nulls.nullability().template Is<core::NonNull>()) {
121+
*view = ColumnView::Reference(type, data, nullptr);
122+
return;
123+
}
124+
const BitVector& bits = nulls.GetNullBitVector();
125+
if (nulls.nullability().template Is<core::DenseNull>()) {
126+
// Already one slot per row, so the values can be read where they lie.
127+
*view = ColumnView::Reference(type, data, &bits);
128+
return;
129+
}
130+
auto impl = std::make_unique<ExpanderImpl<T>>(type, data, &bits);
131+
*owner = impl->owner();
132+
*expander = std::move(impl);
133+
}
134+
135+
} // namespace
136+
137+
DataframeScan::DataframeScan(const dataframe::Dataframe* dataframe,
138+
std::vector<uint32_t> columns)
139+
: dataframe_(dataframe), columns_(std::move(columns)) {}
140+
141+
DataframeScan::~DataframeScan() = default;
142+
DataframeScan::State::~State() = default;
143+
144+
std::unique_ptr<OperatorState> DataframeScan::MakeState() const {
145+
auto state = std::make_unique<State>();
146+
state->columns.resize(columns_.size());
147+
state->owners.resize(columns_.size());
148+
state->expanders.resize(columns_.size());
149+
for (uint32_t i = 0; i < columns_.size(); ++i) {
150+
uint32_t index = columns_[i];
151+
StorageType type = dataframe_->column_type(index);
152+
if (type.Is<Id>()) {
153+
// No storage at all: the value is the row it sits at.
154+
state->columns[i] = ColumnView::Reference(type, nullptr, nullptr);
155+
continue;
156+
}
157+
const dataframe::Column& column = dataframe_->column(index);
158+
if (type.Is<Uint32>()) {
159+
BuildColumn<uint32_t>(column, type, &state->columns[i], &state->owners[i],
160+
&state->expanders[i]);
161+
} else if (type.Is<Int32>()) {
162+
BuildColumn<int32_t>(column, type, &state->columns[i], &state->owners[i],
163+
&state->expanders[i]);
164+
} else if (type.Is<Int64>()) {
165+
BuildColumn<int64_t>(column, type, &state->columns[i], &state->owners[i],
166+
&state->expanders[i]);
167+
} else if (type.Is<Double>()) {
168+
BuildColumn<double>(column, type, &state->columns[i], &state->owners[i],
169+
&state->expanders[i]);
170+
} else {
171+
BuildColumn<StringPool::Id>(column, type, &state->columns[i],
172+
&state->owners[i], &state->expanders[i]);
173+
}
174+
}
175+
return state;
176+
}
177+
178+
void DataframeScan::Rewind(OperatorState& state) const {
179+
State& s = state.Cast<State>();
180+
s.emitted = 0;
181+
for (const std::unique_ptr<Expander>& expander : s.expanders) {
182+
if (expander) {
183+
expander->Rewind();
184+
}
185+
}
186+
}
187+
188+
bool DataframeScan::GetData(RowBatch& out, OperatorState& state) const {
189+
State& s = state.Cast<State>();
190+
uint32_t rows = dataframe_->row_count();
191+
if (s.emitted == rows) {
192+
return false;
193+
}
194+
uint32_t count = std::min(kMaxBatchRows, rows - s.emitted);
195+
out.Reset();
196+
for (uint32_t i = 0; i < s.columns.size(); ++i) {
197+
ColumnView view = s.columns[i];
198+
if (s.expanders[i]) {
199+
// Expanded values are laid out from zero, so the column sits in its own
200+
// index space rather than the dataframe's.
201+
s.expanders[i]->Expand(s.emitted, count, &view);
202+
} else {
203+
view.SetRange(s.emitted);
204+
}
205+
out.AddColumn(view, s.owners[i]);
206+
}
207+
out.SetCardinality(count);
208+
s.emitted += count;
209+
return true;
210+
}
211+
212+
} // namespace perfetto::trace_processor::core::exec
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef SRC_TRACE_PROCESSOR_CORE_EXEC_DATAFRAME_SCAN_H_
18+
#define SRC_TRACE_PROCESSOR_CORE_EXEC_DATAFRAME_SCAN_H_
19+
20+
#include <cstdint>
21+
#include <memory>
22+
#include <vector>
23+
24+
#include "src/trace_processor/core/dataframe/dataframe.h"
25+
#include "src/trace_processor/core/exec/column_view.h"
26+
#include "src/trace_processor/core/exec/operator.h"
27+
#include "src/trace_processor/core/exec/row_batch.h"
28+
29+
namespace perfetto::trace_processor::core::exec {
30+
31+
// Reads a dataframe's rows without going through SQL.
32+
//
33+
// The batches point straight at the dataframe's own storage, so a query which
34+
// reads a table and does nothing else to it copies nothing. Deciding whether a
35+
// query is one of those belongs to whoever builds the plan: a relation which
36+
// filters, joins, groups or computes has work for SQLite to do and goes to
37+
// SqlScan instead.
38+
//
39+
// The exception is a column which does not store one value per row. Such a
40+
// column is expanded a batch at a time into a fixed-size buffer owned by the
41+
// execution, so a relation can be free for most of its columns and pay a
42+
// bounded amount for the rest. Nothing is materialised ahead of being asked
43+
// for, so a query which reads one batch and stops does one batch of work.
44+
class DataframeScan : public Source {
45+
public:
46+
DataframeScan(const dataframe::Dataframe*, std::vector<uint32_t> columns);
47+
~DataframeScan() override;
48+
49+
std::unique_ptr<OperatorState> MakeState() const override;
50+
bool GetData(RowBatch& out, OperatorState& state) const override;
51+
void Rewind(OperatorState& state) const override;
52+
53+
// Fills one batch of a column which does not store one value per row.
54+
// Defined in the .cc: an implementation detail with no callers outside it.
55+
class Expander;
56+
57+
private:
58+
struct State : OperatorState {
59+
~State() override;
60+
std::vector<ColumnView> columns;
61+
// One per column: what keeps an expanded column alive, null where the
62+
// column points at the dataframe's own storage.
63+
std::vector<std::shared_ptr<const void>> owners;
64+
// One per column, null unless the column has to be expanded.
65+
std::vector<std::unique_ptr<Expander>> expanders;
66+
uint32_t emitted = 0;
67+
};
68+
69+
const dataframe::Dataframe* dataframe_;
70+
std::vector<uint32_t> columns_;
71+
};
72+
73+
} // namespace perfetto::trace_processor::core::exec
74+
75+
#endif // SRC_TRACE_PROCESSOR_CORE_EXEC_DATAFRAME_SCAN_H_

0 commit comments

Comments
 (0)