Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
df902b5
poc commit
BrianWhitneyAI Apr 9, 2026
34bc28f
feat: query benchmark system with schema variation and cloud fixture
BrianWhitneyAI Apr 15, 2026
378bf71
Switch benchmark workflow to manual workflow_dispatch
BrianWhitneyAI Apr 15, 2026
2931ebc
refactor: wire benchmark queries to actual service SQL builders
BrianWhitneyAI Apr 15, 2026
521e3db
fix: pass waitForFunction timeout as options arg, not page function arg
BrianWhitneyAI Apr 15, 2026
2352424
fix: use round-robin shuffled timing to eliminate cache dilution betw…
BrianWhitneyAI Apr 15, 2026
7868f2d
chore: use branch names in job titles and comparison table headers
BrianWhitneyAI Apr 15, 2026
9149e11
fix: use BENCHMARK_BRANCH instead of GITHUB_REF_NAME in workflow
BrianWhitneyAI Apr 15, 2026
25e267e
fix: run both benchmarks sequentially on the same CI runner
BrianWhitneyAI Apr 15, 2026
c565ae0
refactor: benchmark against parquet views to match production query path
BrianWhitneyAI Apr 15, 2026
784919e
fix: copy fixture buffer before registerFileBuffer transfers ownership
BrianWhitneyAI Apr 15, 2026
55b6c7b
fix: re-export fixture from view instead of reusing transferred buffer
BrianWhitneyAI Apr 15, 2026
36f86c0
feat: drop 10k scale, expand cloud benchmark to all scales
BrianWhitneyAI Apr 15, 2026
b454a4e
fix: cap cloud fixtures at 1M rows to avoid CDP transfer limit
BrianWhitneyAI Apr 16, 2026
b3d1449
docs: explain why benchmark runs sequentially on the same VM
BrianWhitneyAI Apr 16, 2026
794c039
feat: service-layer task benchmark with query timing instrumentation
BrianWhitneyAI Apr 22, 2026
f49eb3d
fix: move fixture download after checkout, add cache
BrianWhitneyAI Apr 22, 2026
4d209f5
feat: accurate DuckDB-internal query timing for benchmark
BrianWhitneyAI Apr 23, 2026
fb85966
cleanup
BrianWhitneyAI Apr 24, 2026
a0b20a8
merge: resolve conflicts with main
BrianWhitneyAI Apr 24, 2026
ee5c03f
cleanup
BrianWhitneyAI Apr 27, 2026
5bd2bbd
benchmark documentation
BrianWhitneyAI Apr 27, 2026
794eba8
add datetime filter, update badge thresholds
BrianWhitneyAI Apr 27, 2026
e8af72d
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI Apr 27, 2026
47bada0
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI Apr 29, 2026
6a8f88a
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI May 4, 2026
cef7f2b
Update packages/web/src/services/DatabaseServiceWeb/duckdb-worker.wor…
BrianWhitneyAI May 6, 2026
13a5fab
Update packages/web/src/services/DatabaseServiceWeb/duckdb-worker.wor…
BrianWhitneyAI May 6, 2026
5626d91
Update packages/web/src/services/DatabaseServiceWeb/duckdb-worker.wor…
BrianWhitneyAI May 6, 2026
8d7b4ef
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI May 6, 2026
86d73a5
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI May 8, 2026
ba763a0
Remove unnecessary service exports and add benchmark design rationale
BrianWhitneyAI May 8, 2026
f917553
Fix forbidden non-null assertions in benchmark timing loop
BrianWhitneyAI May 8, 2026
a3bfab3
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI May 8, 2026
78d8afa
Merge branch 'main' into feature/query-benchmark
BrianWhitneyAI May 21, 2026
dcb78be
Update packages/web/scripts/summarize-results.js
BrianWhitneyAI May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Query Benchmark

on:
workflow_dispatch:
inputs:
base_branch:
description: "Base branch (reference)"
required: true
type: string
default: "main"
compare_branch:
description: "Branch to compare against base"
required: true
type: string

# Only run one benchmark at a time per base+compare pair
concurrency:
group: benchmark-${{ github.event.inputs.base_branch }}-${{ github.event.inputs.compare_branch }}
cancel-in-progress: true

jobs:
benchmark:
name: Benchmark (${{ github.event.inputs.base_branch }} vs ${{ github.event.inputs.compare_branch }})
runs-on: ubuntu-latest
# Both branches run sequentially in a single job on the same VM. This is intentional:
# if each branch ran in its own job, GitHub could schedule them on different physical
# machines with different CPU speeds, cache sizes, or competing workloads. A ~15%
# hardware variance between VMs would mask the small regressions we actually care about.
# Running back-to-back on the same VM ensures both measurements share the same hardware
# baseline, so deltas reflect code differences only.
# 60 minutes: large-scale table creation (10M rows) + full query suite × 2 branches
timeout-minutes: 60

steps:
# -----------------------------------------------------------------------
# Base branch
# -----------------------------------------------------------------------
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.base_branch }}

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Install Playwright Chromium
run: npx playwright install chromium --with-deps
working-directory: packages/web

- name: Run benchmark (${{ github.event.inputs.base_branch }})
run: node scripts/run-benchmark.js
working-directory: packages/web
env:
BENCHMARK_BRANCH: ${{ github.event.inputs.base_branch }}

- name: Save base results
run: mv packages/web/benchmark-results-*.json /tmp/benchmark-base.json

# -----------------------------------------------------------------------
# Compare branch
# -----------------------------------------------------------------------
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.compare_branch }}
# Preserve untracked files (e.g. node_modules) to speed up npm ci
clean: false

- name: Install dependencies
run: npm ci

- name: Run benchmark (${{ github.event.inputs.compare_branch }})
run: node scripts/run-benchmark.js
working-directory: packages/web
env:
BENCHMARK_BRANCH: ${{ github.event.inputs.compare_branch }}

# -----------------------------------------------------------------------
# Compare results
# -----------------------------------------------------------------------
- name: Generate comparison
run: |
COMPARE_FILE=$(ls packages/web/benchmark-results-*.json | head -1)
node packages/web/scripts/compare-results.js /tmp/benchmark-base.json "$COMPARE_FILE" >> "$GITHUB_STEP_SUMMARY"

- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: benchmark-results
path: |
/tmp/benchmark-base.json
packages/web/benchmark-results-*.json
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ build
*.tgz
.env
mise.toml

# Benchmark runner output — generated by CI, not source
packages/web/benchmark-results*.json
50 changes: 49 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import FileFilter from "../../../entity/FileFilter";
import IncludeFilter from "../../../entity/FileFilter/IncludeFilter";
import SQLBuilder from "../../../entity/SQLBuilder";

/**
* SQL used by fetchFilteredValuesForAnnotation — exported so the benchmark can run the same query.
* Filters are optional; pass none for an unfiltered distinct-values query.
*/
export function buildDistinctValuesSQL(
Comment thread
BrianWhitneyAI marked this conversation as resolved.
Outdated
annotation: string,
dataSourceNames: string | string[],
filters: FileFilter[] = []
): string {
const builder = new SQLBuilder().select(`DISTINCT "${annotation}"`).from(dataSourceNames);
const filtersByAnnotation = filters.reduce(
(map, filter) => ({
...map,
[filter.name]: map[filter.name] ? [...map[filter.name], filter] : [filter],
}),
{} as { [name: string]: FileFilter[] }
);
Object.values(filtersByAnnotation).forEach((appliedFilters) => {
builder.where(appliedFilters.map((f) => f.toSQLWhereString()).join(" OR "));
});
return builder.toSQL();
}

interface Config {
databaseService: DatabaseService;
dataSourceNames: string[];
Expand Down Expand Up @@ -112,18 +135,10 @@ export default class DatabaseAnnotationService implements AnnotationService {
return [];
}

const sqlBuilder = new SQLBuilder()
.select(`DISTINCT "${annotation}"`)
.from(this.dataSourceNames);

Object.keys(filtersByAnnotation).forEach((annotationToFilter) => {
const appliedFilters = filtersByAnnotation[annotationToFilter];
sqlBuilder.where(
appliedFilters.map((filter) => filter.toSQLWhereString()).join(" OR ")
);
});

const rows = await this.databaseService.query(sqlBuilder.toSQL()).promise;
const allFilters = Object.values(filtersByAnnotation).flat();
const rows = await this.databaseService.query(
buildDistinctValuesSQL(annotation, this.dataSourceNames, allFilters)
).promise;
const rowsSplitByDelimiter = rows
.flatMap((row) =>
isNil(row[annotation])
Expand Down
17 changes: 11 additions & 6 deletions packages/core/services/DatabaseService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ export function getParquetFileNameSelectPart(
return `${getFileNameFromPathExpression(`"${pathColumn}"`)} AS "${PreDefinedColumn.FILE_NAME}"`;
}

/** SQL used by fetchAnnotations — exported so the benchmark can run the same query. */
export function buildFetchAnnotationsSQL(tableName: string): string {
Comment thread
BrianWhitneyAI marked this conversation as resolved.
Outdated
return new SQLBuilder()
.select("column_name, data_type")
.from('information_schema"."columns')
.where(`table_name = '${tableName}'`)
.where(`column_name != '${HIDDEN_UID_ANNOTATION}'`)
.toSQL();
}

export async function initializeDuckDB(logLevel: duckdb.LogLevel): Promise<duckdb.AsyncDuckDB> {
const allBundles = duckdb.getJsDelivrBundles();

Expand Down Expand Up @@ -1032,12 +1042,7 @@ export default abstract class DatabaseService {
?.some((annotation) => !!annotation.description);
const shouldHaveDescriptions = dataSourceNames.includes(this.SOURCE_METADATA_TABLE);
if (!hasAnnotations || (!hasDescriptions && shouldHaveDescriptions)) {
const sql = new SQLBuilder()
.select("column_name, data_type")
.from('information_schema"."columns')
.where(`table_name = '${aggregateDataSourceName}'`)
.where(`column_name != '${HIDDEN_UID_ANNOTATION}'`)
.toSQL();
const sql = buildFetchAnnotationsSQL(aggregateDataSourceName);
const rows = await this.query(sql).promise;
if (isEmpty(rows)) {
throw new Error(`Unable to fetch annotations for ${aggregateDataSourceName}`);
Expand Down
48 changes: 35 additions & 13 deletions packages/core/services/FileService/DatabaseFileService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ import FileDetail from "../../../entity/FileDetail";
import SQLBuilder from "../../../entity/SQLBuilder";
import { Environment, HIDDEN_UID_ANNOTATION } from "../../../constants";

/**
* SQL used by getFiles — exported so the benchmark can run the same query.
* `from` is a page index (0-based); the row offset is `from * limit`.
*/
export function buildGetFilesSQL(
dataSourceNames: string | string[],
fileSet: FileSet,
from: number,
limit: number
): string {
return fileSet
.toQuerySQLBuilder()
.from(dataSourceNames)
.offset(from * limit)
.limit(limit)
.toSQL();
}

/** SQL used by getCountOfMatchingFiles — exported so the benchmark can run the same query. */
export function buildGetCountSQL(dataSourceNames: string | string[], fileSet: FileSet): string {
return fileSet
.toQuerySQLBuilder()
.select("COUNT(*) AS num_files")
.from(dataSourceNames)
.removeOrderBy()
.toSQL();
}

interface Config {
databaseService: DatabaseService;
dataSourceNames: string[];
Expand Down Expand Up @@ -93,13 +121,7 @@ export default class DatabaseFileService implements FileService {
}

const select_key = "num_files";
const sql = fileSet
.toQuerySQLBuilder()
.select(`COUNT(*) AS ${select_key}`)
.from(this.dataSourceNames)
// Remove sort if present
.removeOrderBy()
.toSQL();
const sql = buildGetCountSQL(this.dataSourceNames, fileSet);

const rows = await this.databaseService.query(sql).promise;
return parseInt(rows[0][select_key], 10);
Expand All @@ -125,12 +147,12 @@ export default class DatabaseFileService implements FileService {
if (!this.dataSourceNames.length) {
return [];
}
const sql = request.fileSet
.toQuerySQLBuilder()
.from(this.dataSourceNames)
.offset(request.from * request.limit)
.limit(request.limit)
.toSQL();
const sql = buildGetFilesSQL(
this.dataSourceNames,
request.fileSet,
request.from,
request.limit
);

const rows = await this.databaseService.query(sql).promise;
const env = this.downloadService.getEnvironmentFromUrl();
Expand Down
10 changes: 10 additions & 0 deletions packages/web/benchmark/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>BFF Benchmark</title>
</head>
<body>
<p id="status">Starting...</p>
</body>
</html>
Loading
Loading