Skip to content

Commit 6724c02

Browse files
committed
Profiler driven optimization for perspective-server
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent 82eaa40 commit 6724c02

22 files changed

Lines changed: 317 additions & 131 deletions

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ catalog:
4242
# Dev Dependencies
4343
"@clickhouse/client-web": "^1.12.0"
4444
"@duckdb/duckdb-wasm": "1.33.1-dev18.0"
45-
"@duckdb/duckdb-wasm-shell": "^1.30.0"
4645
"@fontsource/roboto-mono": "4.5.10"
4746
"@iarna/toml": "3.0.0"
4847
"@jupyterlab/builder": "^4"

rust/perspective-server/cpp/perspective/CMakeLists.txt

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -576,23 +576,12 @@ else()
576576
# -s MEMORY_GROWTH_GEOMETRIC_STEP=1.0 \
577577
# -s MEMORY_GROWTH_GEOMETRIC_CAP=536870912 \
578578

579+
set(PSP_FILESYSTEM_FLAGS "-s NO_FILESYSTEM=1 ")
580+
set(PSP_CLOSURE_FLAGS "--closure=1 ")
581+
579582
# On-disk tables in the browser are backed by OPFS via emscripten's WasmFS.
580-
# `PSP_WASM_OPFS` swaps the default `NO_FILESYSTEM` (memory-only) build for
581-
# `WASMFS` + `FORCE_FILESYSTEM`, and enables the OPFS backend. `JSPI` provides
582-
# the synchronous-looking OPFS access the WasmFS OPFS backend needs without
583-
# SharedArrayBuffer/cross-origin-isolation (`-pthread`). `JSPI` is backwards
584-
# compatible: the module loads and runs in-memory tables fine on non-JSPI
585-
# browsers (it has no `main`/async imports wrapped at instantiation); the JSPI
586-
# API is only reached when an actual OPFS op runs, i.e. when `page_to_disk` is used
587-
# (the JS client gates that on JSPI being present). Closure is incompatible
588-
# with the WasmFS JS glue, so it is disabled in that case.
589583
if(PSP_WASM_OPFS)
590-
set(PSP_FILESYSTEM_FLAGS "-s NO_FILESYSTEM=1 ")
591-
set(PSP_CLOSURE_FLAGS "--closure=1 ")
592584
add_compile_definitions(PSP_WASM_OPFS_PERSIST=1)
593-
else()
594-
set(PSP_FILESYSTEM_FLAGS "-s NO_FILESYSTEM=1 ")
595-
set(PSP_CLOSURE_FLAGS "--closure=1 ")
596585
endif()
597586

598587
set(PSP_WASM_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \

rust/perspective-server/cpp/perspective/src/cpp/arrow_loader.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -934,9 +934,7 @@ ArrowLoader::fill_column(
934934
std::int64_t null_count = array->null_count();
935935

936936
if (null_count == 0) {
937-
for (uint32_t i = 0; i < len; ++i) {
938-
col->set_valid(offset + i, true);
939-
}
937+
col->set_valid_range(offset, len);
940938
} else {
941939
const uint8_t* null_bitmap = array->null_bitmap_data();
942940

rust/perspective-server/cpp/perspective/src/cpp/column.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,17 @@ t_column::set_status(t_uindex idx, t_status status) {
620620
m_status->set_nth<t_status>(idx, status);
621621
}
622622

623+
void
624+
t_column::set_valid_range(t_uindex offset, t_uindex len) {
625+
if (!is_status_enabled() || len == 0) {
626+
return;
627+
}
628+
static_assert(
629+
sizeof(t_status) == 1, "set_valid_range assumes a 1-byte t_status"
630+
);
631+
std::memset(m_status->get_nth<t_status>(offset), STATUS_VALID, len);
632+
}
633+
623634
void
624635
t_column::set_scalar(t_uindex idx, t_tscalar value) {
625636
COLUMN_CHECK_ACCESS(idx);

rust/perspective-server/cpp/perspective/src/cpp/computed_expression.cpp

Lines changed: 105 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ t_computed_expression::t_computed_expression(
102102
m_column_ids(column_ids),
103103
m_dtype(dtype) {}
104104

105+
struct t_computed_expression_cache {
106+
t_computed_expression_cache() = default;
107+
t_computed_expression_cache(const t_computed_expression_cache&) = delete;
108+
t_computed_expression_cache& operator=(const t_computed_expression_cache&) =
109+
delete;
110+
t_computed_expression_cache(t_computed_expression_cache&&) = delete;
111+
t_computed_expression_cache& operator=(t_computed_expression_cache&&) =
112+
delete;
113+
114+
std::shared_ptr<t_data_table> m_current_source_table;
115+
t_uindex m_row_idx{0};
116+
std::vector<std::pair<std::string, t_tscalar>> m_values;
117+
std::vector<std::shared_ptr<t_column>> m_columns;
118+
std::vector<t_dtype> m_input_dtypes;
119+
exprtk::symbol_table<t_tscalar> m_sym_table;
120+
exprtk::expression<t_tscalar> m_expr;
121+
std::unique_ptr<t_computed_function_store> m_function_store;
122+
};
123+
124+
t_computed_expression::~t_computed_expression() = default;
125+
105126
void
106127
t_computed_expression::compute(
107128
const std::shared_ptr<t_data_table>& source_table,
@@ -110,53 +131,94 @@ t_computed_expression::compute(
110131
t_expression_vocab& vocab,
111132
t_regex_mapping& regex_mapping
112133
) const {
113-
// TODO: share symtables across pre/re/compute
114-
exprtk::symbol_table<t_tscalar> sym_table;
134+
auto num_input_columns = m_column_ids.size();
115135

116-
// pi, infinity, etc.
117-
sym_table.add_constants();
136+
bool needs_build = !m_cache;
137+
if (!needs_build) {
138+
for (t_uindex cidx = 0; cidx < num_input_columns; ++cidx) {
139+
const std::string& column_name = m_column_ids[cidx].second;
140+
if (source_table->get_column(column_name)->get_dtype()
141+
!= m_cache->m_input_dtypes[cidx]) {
142+
needs_build = true;
143+
break;
144+
}
145+
}
146+
}
118147

119-
t_uindex row_idx = 0;
148+
if (needs_build) {
149+
m_cache = std::make_unique<t_computed_expression_cache>();
150+
auto& cache = *m_cache;
151+
152+
cache.m_current_source_table = source_table;
153+
cache.m_sym_table.add_constants(); // pi, infinity, etc.
154+
155+
// is_type_validator = false: we are computing values, not type-checking.
156+
cache.m_function_store = std::make_unique<t_computed_function_store>(
157+
vocab,
158+
regex_mapping,
159+
false,
160+
cache.m_current_source_table,
161+
pkey_map,
162+
cache.m_row_idx
163+
);
164+
cache.m_function_store->register_computed_functions(cache.m_sym_table);
120165

121-
// Create a function store, with is_type_validator set to false as we
122-
// are calculating values, not type-checking.
123-
t_computed_function_store function_store(
124-
vocab, regex_mapping, false, source_table, pkey_map, row_idx
125-
);
126-
function_store.register_computed_functions(sym_table);
166+
// Size exactly once; the symbol table binds a T* into each
167+
// m_values[cidx].second, so this storage must never be reallocated.
168+
cache.m_values.resize(num_input_columns);
169+
cache.m_columns.resize(num_input_columns);
170+
cache.m_input_dtypes.resize(num_input_columns);
127171

128-
exprtk::expression<t_tscalar> expr_definition;
129-
std::vector<std::pair<std::string, t_tscalar>> values;
130-
tsl::hopscotch_map<std::string, std::shared_ptr<t_column>> columns;
172+
for (t_uindex cidx = 0; cidx < num_input_columns; ++cidx) {
173+
const std::string& column_id = m_column_ids[cidx].first;
174+
const std::string& column_name = m_column_ids[cidx].second;
175+
t_dtype dtype = source_table->get_column(column_name)->get_dtype();
131176

132-
auto num_input_columns = m_column_ids.size();
133-
values.resize(num_input_columns);
134-
columns.reserve(num_input_columns);
177+
cache.m_input_dtypes[cidx] = dtype;
135178

136-
for (t_uindex cidx = 0; cidx < num_input_columns; ++cidx) {
137-
const std::string& column_id = m_column_ids[cidx].first;
138-
const std::string& column_name = m_column_ids[cidx].second;
139-
columns[column_id] = source_table->get_column(column_name);
179+
t_tscalar rval;
180+
rval.clear();
181+
rval.m_type = dtype;
140182

141-
t_tscalar rval;
142-
rval.clear();
143-
rval.m_type = columns[column_id]->get_dtype();
183+
cache.m_values[cidx] =
184+
std::pair<std::string, t_tscalar>(column_id, rval);
185+
cache.m_sym_table.add_variable(
186+
column_id, cache.m_values[cidx].second
187+
);
188+
}
144189

145-
values[cidx] = std::pair<std::string, t_tscalar>(column_id, rval);
146-
sym_table.add_variable(column_id, values[cidx].second);
190+
cache.m_expr.register_symbol_table(cache.m_sym_table);
191+
192+
// Load-bearing for compile-once: exprtk constant-folds an all-literal
193+
// function call (e.g. intern('x'), concat('a','b')) to a literal node at
194+
// compile time ONLY when the function reports no side effects. Our
195+
// functions inherit the igeneric_function/ifunction default
196+
// has_side_effects() == true and never call disable_has_side_effects(),
197+
// so such calls are not folded and re-evaluate on every value(). If a
198+
// future exprtk bump flips that default, the first call's result would
199+
// be frozen into the cached AST -- revisit this cache if so.
200+
if (!m_computed_expression_parser.m_parser->compile(
201+
m_parsed_expression_string, cache.m_expr
202+
)) {
203+
std::stringstream ss;
204+
ss << "[t_computed_expression::compute] Failed to parse "
205+
"expression: `"
206+
<< m_parsed_expression_string << "`, failed with error: "
207+
<< m_computed_expression_parser.m_parser->error() << '\n';
208+
209+
PSP_COMPLAIN_AND_ABORT(ss.str());
210+
}
147211
}
148212

149-
expr_definition.register_symbol_table(sym_table);
150-
151-
if (!m_computed_expression_parser.m_parser->compile(
152-
m_parsed_expression_string, expr_definition
153-
)) {
154-
std::stringstream ss;
155-
ss << "[t_computed_expression::compute] Failed to parse expression: `"
156-
<< m_parsed_expression_string << "`, failed with error: "
157-
<< m_computed_expression_parser.m_parser->error() << '\n';
213+
auto& cache = *m_cache;
158214

159-
PSP_COMPLAIN_AND_ABORT(ss.str());
215+
// Re-point the per-call inputs the compiled function objects read through
216+
// (the source table differs across the master/flattened/delta/prev/current
217+
// tables within a single update), and re-fetch this call's source columns.
218+
cache.m_current_source_table = source_table;
219+
for (t_uindex cidx = 0; cidx < num_input_columns; ++cidx) {
220+
cache.m_columns[cidx] =
221+
source_table->get_column(m_column_ids[cidx].second);
160222
}
161223

162224
// create or get output column using m_expression_alias
@@ -167,12 +229,13 @@ t_computed_expression::compute(
167229

168230
for (t_uindex ridx = 0; ridx < num_rows; ++ridx) {
169231
for (t_uindex cidx = 0; cidx < num_input_columns; ++cidx) {
170-
const std::string& column_id = m_column_ids[cidx].first;
171-
values[cidx].second.set(columns[column_id]->get_scalar(ridx));
232+
cache.m_values[cidx].second.set(
233+
cache.m_columns[cidx]->get_scalar(ridx)
234+
);
172235
}
173-
row_idx = ridx;
236+
cache.m_row_idx = ridx;
174237

175-
t_tscalar value = expr_definition.value();
238+
t_tscalar value = cache.m_expr.value();
176239

177240
if (!value.is_valid() || value.is_none()) {
178241
output_column->clear(ridx);
@@ -182,7 +245,8 @@ t_computed_expression::compute(
182245
output_column->set_scalar(ridx, value);
183246
}
184247

185-
function_store.clear_computed_function_state();
248+
// order()'s accumulator must still be reset at the end of every call.
249+
cache.m_function_store->clear_computed_function_state();
186250
};
187251

188252
const std::string&

rust/perspective-server/cpp/perspective/src/cpp/computed_function.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2330,12 +2330,12 @@ make_datetime::operator()(t_parameter_list parameters) {
23302330

23312331
index::index(
23322332
const t_gstate::t_mapping& pkey_map,
2333-
std::shared_ptr<t_data_table> source_table,
2333+
const std::shared_ptr<t_data_table>& source_table,
23342334
t_uindex& row_idx
23352335
) :
23362336
exprtk::igeneric_function<t_tscalar>("Z"),
23372337
m_pkey_map(pkey_map),
2338-
m_source_table(std::move(std::move(source_table))),
2338+
m_source_table(source_table),
23392339
m_row_idx(row_idx) {}
23402340

23412341
index::~index() = default;
@@ -2355,13 +2355,13 @@ index::operator()(t_parameter_list parameters) {
23552355
col::col(
23562356
t_expression_vocab& expression_vocab,
23572357
bool is_type_validator,
2358-
std::shared_ptr<t_data_table> source_table,
2358+
const std::shared_ptr<t_data_table>& source_table,
23592359
t_uindex& row_idx
23602360
) :
23612361
exprtk::igeneric_function<t_tscalar>("T"),
23622362
m_expression_vocab(expression_vocab),
23632363
m_is_type_validator(is_type_validator),
2364-
m_source_table(std::move(std::move(source_table))),
2364+
m_source_table(source_table),
23652365
m_row_idx(row_idx) {}
23662366
col::~col() = default;
23672367

@@ -2394,13 +2394,13 @@ col::operator()(t_parameter_list parameters) {
23942394
vlookup::vlookup(
23952395
t_expression_vocab& expression_vocab,
23962396
bool is_type_validator,
2397-
std::shared_ptr<t_data_table> source_table,
2397+
const std::shared_ptr<t_data_table>& source_table,
23982398
t_uindex& row_idx
23992399
) :
24002400
exprtk::igeneric_function<t_tscalar>("TT"),
24012401
m_expression_vocab(expression_vocab),
24022402
m_is_type_validator(is_type_validator),
2403-
m_source_table(std::move(std::move(source_table))),
2403+
m_source_table(source_table),
24042404
m_row_idx(row_idx) {}
24052405
vlookup::~vlookup() = default;
24062406

rust/perspective-server/cpp/perspective/src/cpp/context_grouped_pkey.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ t_ctx_grouped_pkey::get_data(
152152
}
153153

154154
auto* aggtable = m_tree->get_aggtable();
155-
t_schema aggschema = aggtable->get_schema();
156155

157156
for (t_uindex aggidx = 0, loop_end = aggcols.size(); aggidx < loop_end;
158157
++aggidx) {
159-
const std::string& aggname = aggschema.m_columns[aggidx];
160-
aggcols[aggidx] = aggtable->_get_const_column(aggname);
158+
// resolve by column index (== aggidx); skips the per-iteration
159+
// name->index map lookup + temp string and the schema deep-copy.
160+
aggcols[aggidx] = aggtable->_get_const_column(aggidx);
161161
}
162162

163163
const std::vector<t_aggspec>& aggspecs = m_config.get_aggregates();

0 commit comments

Comments
 (0)