diff --git a/keyten-parquet/.gitignore b/keyten-parquet/.gitignore new file mode 100644 index 0000000000..70a523130d --- /dev/null +++ b/keyten-parquet/.gitignore @@ -0,0 +1,7 @@ +myenv/ +__pycache__/ +hits.parquet +hits.k10dir/ +server.pid +server.log +result.csv diff --git a/keyten-parquet/README.md b/keyten-parquet/README.md new file mode 100644 index 0000000000..c631918035 --- /dev/null +++ b/keyten-parquet/README.md @@ -0,0 +1,10 @@ +# Keyten over Parquet + +Runs the same dataframe expressions as `../keyten` directly over the single +official Parquet file, without converting it to native storage. The client +times the request through receipt of the complete serialized result. + +Run `./benchmark.sh` from this directory. Shared scripts and queries are linked +to the native-storage entry so query semantics and timing stay identical. +The standard driver restarts the server and drops OS caches before each cold +query. The server opens the file only on its first timed query. diff --git a/keyten-parquet/benchmark.sh b/keyten-parquet/benchmark.sh new file mode 100755 index 0000000000..d6617aecd1 --- /dev/null +++ b/keyten-parquet/benchmark.sh @@ -0,0 +1,5 @@ +#!/bin/bash +export BENCH_DOWNLOAD_SCRIPT=download-hits-parquet-single +export BENCH_RESTARTABLE=yes +export BENCH_DURABLE=yes +exec ../lib/benchmark-common.sh diff --git a/keyten-parquet/check b/keyten-parquet/check new file mode 120000 index 0000000000..ba5bc869e7 --- /dev/null +++ b/keyten-parquet/check @@ -0,0 +1 @@ +../keyten/check \ No newline at end of file diff --git a/keyten-parquet/data-size b/keyten-parquet/data-size new file mode 100755 index 0000000000..f47225dc63 --- /dev/null +++ b/keyten-parquet/data-size @@ -0,0 +1,3 @@ +#!/bin/bash +set -eu +stat -c '%s' hits.parquet diff --git a/keyten-parquet/install b/keyten-parquet/install new file mode 120000 index 0000000000..94fb7501a3 --- /dev/null +++ b/keyten-parquet/install @@ -0,0 +1 @@ +../keyten/install \ No newline at end of file diff --git a/keyten-parquet/load b/keyten-parquet/load new file mode 100755 index 0000000000..f4ebf0bbaf --- /dev/null +++ b/keyten-parquet/load @@ -0,0 +1,4 @@ +#!/bin/bash +set -eu +# Stateless: the first timed query opens the source file. +test -s hits.parquet diff --git a/keyten-parquet/queries.sql b/keyten-parquet/queries.sql new file mode 120000 index 0000000000..da5b562747 --- /dev/null +++ b/keyten-parquet/queries.sql @@ -0,0 +1 @@ +../keyten/queries.sql \ No newline at end of file diff --git a/keyten-parquet/query b/keyten-parquet/query new file mode 120000 index 0000000000..4cc6f201cf --- /dev/null +++ b/keyten-parquet/query @@ -0,0 +1 @@ +../keyten/query \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c6a.2xlarge.json b/keyten-parquet/results/20261002/c6a.2xlarge.json new file mode 100644 index 0000000000..ddd4c63412 --- /dev/null +++ b/keyten-parquet/results/20261002/c6a.2xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c6a.2xlarge", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 0.082, + "concurrent_error_ratio": 0.65, + "result": [ + [0.103, 0.031, 0.031], + [0.158, 0.056, 0.056], + [0.436, 0.289, 0.279], + [0.639, 0.24, 0.249], + [1.429, 1.092, 1.091], + [1.9, 1.517, 1.495], + [0.117, 0.032, 0.032], + [0.192, 0.059, 0.064], + [1.828, 1.365, 1.319], + [2.245, 1.666, 1.614], + [0.813, 0.346, 0.333], + [0.841, 0.388, 0.367], + [1.916, 1.469, 1.463], + [3.717, 2.792, 2.733], + [2.073, 1.557, 1.472], + [1.792, 1.349, 1.348], + [3.415, 2.738, 2.744], + [5.816, 4.803, 4.747], + [6.536, 5.526, 5.525], + [0.429, 0.097, 0.095], + [10.176, 3.117, 2.912], + [11.973, 4.292, 4.275], + [47.922, 137.432, 191.578], + [13.246, 3.492, 3.465], + [2.438, 1.005, 1.007], + [1.153, 0.809, 0.803], + [2.434, 1.021, 1.02], + [10.318, 3.069, 2.95], + [19.588, 18.317, 85.391], + [0.341, 0.199, 0.195], + [2.606, 1.64, 1.607], + [6.294, 2.007, 2.003], + [10.189, 8.983, 9.124], + [11.423, 5.707, 5.616], + [11.436, 5.799, 5.472], + [1.763, 1.382, 1.389], + [0.395, 0.194, 0.17], + [0.314, 0.159, 0.157], + [0.319, 0.12, 0.122], + [0.876, 0.504, 0.485], + [0.211, 0.066, 0.064], + [0.196, 0.054, 0.05], + [0.193, 0.065, 0.071] +] + } + \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c6a.4xlarge.json b/keyten-parquet/results/20261002/c6a.4xlarge.json new file mode 100644 index 0000000000..536b50bdbd --- /dev/null +++ b/keyten-parquet/results/20261002/c6a.4xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c6a.4xlarge", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 0.108, + "concurrent_error_ratio": 0, + "result": [ + [0.108, 0.031, 0.031], + [0.151, 0.046, 0.045], + [0.325, 0.21, 0.195], + [0.423, 0.245, 0.242], + [0.861, 0.648, 0.653], + [1.115, 0.838, 0.835], + [0.12, 0.031, 0.031], + [0.172, 0.048, 0.047], + [1.216, 0.895, 0.863], + [1.474, 1.069, 1.071], + [0.524, 0.218, 0.217], + [0.53, 0.223, 0.219], + [1.446, 1.083, 1.045], + [3.327, 2.077, 2.12], + [1.491, 1.123, 1.176], + [1.235, 1.051, 1.031], + [3.134, 2.099, 2.127], + [5.333, 4.109, 4.124], + [6.246, 4.384, 4.365], + [0.283, 0.094, 0.098], + [10.168, 2.087, 1.983], + [11.889, 2.4, 2.567], + [22.155, 4.856, 4.608], + [13.446, 2.425, 2.393], + [2.434, 0.557, 0.555], + [0.721, 0.439, 0.437], + [2.44, 0.563, 0.561], + [10.316, 2.212, 1.947], + [11.765, 10.432, 11.362], + [0.268, 0.155, 0.159], + [2.683, 1.147, 1.148], + [6.146, 1.422, 1.436], + [7.812, 6.427, 6.432], + [11.214, 3.776, 3.56], + [11.283, 3.747, 3.724], + [1.377, 1.101, 1.095], + [0.407, 0.176, 0.181], + [0.309, 0.159, 0.149], + [0.298, 0.127, 0.113], + [0.834, 0.494, 0.498], + [0.213, 0.062, 0.06], + [0.197, 0.054, 0.05], + [0.202, 0.067, 0.066] +] + } + \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c6a.large.json b/keyten-parquet/results/20261002/c6a.large.json new file mode 100644 index 0000000000..e66fe29101 --- /dev/null +++ b/keyten-parquet/results/20261002/c6a.large.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c6a.large", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 0.015, + "concurrent_error_ratio": 0.438, + "result": [ + [0.111, 0.032, 0.033], + [0.288, 0.091, 0.091], + [1.332, 0.864, 0.865], + [1.915, 0.564, 0.564], + [6.504, 5.437, 5.47], + [6.663, 5.777, 5.784], + [0.112, 0.032, 0.031], + [0.3, 0.092, 0.092], + [5.959, 4.597, 4.582], + [7.255, 5.651, 5.616], + [2.256, 1.008, 1.007], + [2.258, 1.066, 1.06], + [5.63, 4.699, 4.701], + [48.583, 78.201, 87.033], + [6.11, 5.056, 5.051], + [5.189, 4.117, 4.11], + [11.282, 13.328, 19.17], + [141.609, 262.002, 267.891], + [229.63, 192.664, 288.11], + [1.299, 0.236, 0.236], + [14.522, 8.433, 8.439], + [26.557, 56.798, 68.922], + [83.905, 203.756, 242.459], + [18.745, 11.243, 11.109], + [5.701, 3.803, 3.804], + [3.779, 3.098, 3.072], + [5.646, 3.861, 3.869], + [14.608, 8.904, 8.62], + [1182.597, null, null], + [0.735, 0.515, 0.511], + [7.397, 5.498, 5.594], + [12.514, 6.806, 6.765], + [null, null, null], + [1128.361, null, null], + [1126.217, null, null], + [4.566, 3.986, 4.002], + [0.554, 0.301, 0.313], + [0.396, 0.235, 0.224], + [0.381, 0.156, 0.143], + [1.212, 0.806, 0.761], + [0.262, 0.08, 0.076], + [0.238, 0.064, 0.065], + [0.229, 0.08, 0.08] +] + } + \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c6a.metal.json b/keyten-parquet/results/20261002/c6a.metal.json new file mode 100644 index 0000000000..09229f2ac8 --- /dev/null +++ b/keyten-parquet/results/20261002/c6a.metal.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c6a.metal", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 3.603, + "concurrent_error_ratio": 0.005, + "result": [ + [0.125, 0.031, 0.03], + [0.187, 0.042, 0.038], + [0.314, 0.109, 0.101], + [0.434, 0.117, 0.122], + [0.686, 0.186, 0.161], + [0.868, 0.297, 0.26], + [0.108, 0.031, 0.031], + [0.173, 0.047, 0.044], + [0.845, 0.405, 0.433], + [1.254, 0.475, 0.477], + [0.533, 0.151, 0.158], + [0.606, 0.138, 0.145], + [1.014, 0.438, 0.387], + [2.822, 0.94, 0.915], + [1.095, 0.449, 0.434], + [0.925, 0.656, 0.646], + [2.809, 1.268, 1.114], + [5.088, 2.9, 2.876], + [5.422, 2.12, 2.046], + [0.3, 0.073, 0.067], + [9.516, 0.693, 0.52], + [11.33, 1.015, 0.971], + [22.087, 1.774, 1.884], + [13.152, 1.575, 1.624], + [2.466, 0.265, 0.233], + [0.766, 0.212, 0.19], + [2.451, 0.234, 0.226], + [9.697, 0.918, 0.935], + [9.749, 2.882, 2.846], + [0.279, 0.08, 0.074], + [2.391, 0.478, 0.459], + [5.938, 0.654, 0.647], + [6.345, 3.037, 2.995], + [10.23, 1.853, 1.669], + [10.181, 1.773, 1.582], + [1.101, 0.853, 0.848], + [0.411, 0.198, 0.197], + [0.318, 0.172, 0.176], + [0.304, 0.13, 0.127], + [0.852, 0.481, 0.499], + [0.242, 0.07, 0.063], + [0.227, 0.058, 0.05], + [0.229, 0.064, 0.067] +] + } + \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c6a.xlarge.json b/keyten-parquet/results/20261002/c6a.xlarge.json new file mode 100644 index 0000000000..e946f85150 --- /dev/null +++ b/keyten-parquet/results/20261002/c6a.xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c6a.xlarge", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 0.033, + "concurrent_error_ratio": 0, + "result": [ + [0.088, 0.032, 0.032], + [0.18, 0.069, 0.069], + [0.769, 0.473, 0.472], + [1.125, 0.334, 0.337], + [2.797, 2.191, 2.195], + [3.583, 2.979, 2.961], + [0.113, 0.032, 0.032], + [0.222, 0.073, 0.068], + [3.269, 2.477, 2.465], + [3.905, 3.042, 3.038], + [1.229, 0.541, 0.551], + [1.283, 0.606, 0.59], + [3.288, 2.575, 2.593], + [6.42, 4.779, 4.848], + [3.614, 2.946, 2.916], + [2.914, 2.33, 2.289], + [6.16, 4.948, 5.005], + [9.182, 7.867, 21.711], + [11.765, 9.919, 10.068], + [0.705, 0.139, 0.138], + [9.837, 5.023, 4.936], + [13.61, 9.782, 9.346], + [72.782, 201.458, 327.645], + [13.138, 5.642, 5.564], + [2.937, 1.922, 1.922], + [2.058, 1.555, 1.552], + [3.046, 1.965, 1.964], + [10.038, 4.983, 5.064], + [330.187, 634.951, 999.747], + [0.458, 0.31, 0.308], + [4.282, 3.055, 3.054], + [6.883, 3.626, 3.638], + [182.634, 247.265, 266.962], + [193.675, 529.26, 1143.816], + [344.772, 873.726, 1076.579], + [2.639, 2.144, 2.13], + [0.4, 0.22, 0.221], + [0.336, 0.158, 0.174], + [0.322, 0.113, 0.12], + [0.934, 0.558, 0.576], + [0.219, 0.062, 0.056], + [0.207, 0.057, 0.056], + [0.208, 0.071, 0.066] +] + } + \ No newline at end of file diff --git a/keyten-parquet/results/20261002/c7a.metal-48xl.json b/keyten-parquet/results/20261002/c7a.metal-48xl.json new file mode 100644 index 0000000000..9feb2f046e --- /dev/null +++ b/keyten-parquet/results/20261002/c7a.metal-48xl.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten (Parquet)", + "date": "2026-10-02", + "machine": "c7a.metal-48xl", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","stateless"], + "load_time": 0, + "data_size": 14779976446, + "concurrent_qps": 4.11, + "concurrent_error_ratio": 0.004, + "result": [ + [0.116, 0.029, 0.029], + [0.173, 0.04, 0.035], + [0.29, 0.073, 0.07], + [0.351, 0.088, 0.075], + [0.556, 0.141, 0.117], + [1.082, 0.239, 0.21], + [0.116, 0.028, 0.028], + [0.179, 0.041, 0.037], + [0.763, 0.328, 0.299], + [1.224, 0.348, 0.332], + [0.52, 0.115, 0.108], + [0.522, 0.117, 0.107], + [0.902, 0.271, 0.251], + [2.71, 0.776, 0.759], + [0.968, 0.291, 0.284], + [0.67, 0.394, 0.377], + [2.375, 0.616, 0.593], + [4.625, 2.524, 2.441], + [4.4, 1.015, 0.991], + [0.311, 0.056, 0.051], + [9.523, 0.632, 0.636], + [11.336, 0.889, 0.822], + [22.067, 1.521, 1.27], + [13.071, 1.353, 1.38], + [2.461, 0.18, 0.172], + [0.76, 0.149, 0.139], + [2.451, 0.187, 0.174], + [9.705, 0.622, 0.621], + [9.526, 2.518, 2.324], + [0.268, 0.064, 0.055], + [2.258, 0.278, 0.267], + [5.698, 0.364, 0.32], + [5.237, 1.534, 1.44], + [9.974, 1.096, 1.066], + [9.975, 1.19, 1.112], + [0.914, 0.619, 0.592], + [0.444, 0.163, 0.178], + [0.336, 0.147, 0.143], + [0.329, 0.104, 0.105], + [0.883, 0.468, 0.433], + [0.229, 0.057, 0.057], + [0.209, 0.054, 0.045], + [0.217, 0.063, 0.062] +] + } + \ No newline at end of file diff --git a/keyten-parquet/start b/keyten-parquet/start new file mode 100755 index 0000000000..81cb6e7baa --- /dev/null +++ b/keyten-parquet/start @@ -0,0 +1,4 @@ +#!/bin/bash +export KEYTEN_PARQUET="$PWD/hits.parquet" +export KEYTEN_SERVER_SCRIPT=../keyten/server.py +exec ../keyten/start diff --git a/keyten-parquet/stop b/keyten-parquet/stop new file mode 120000 index 0000000000..f5646d9638 --- /dev/null +++ b/keyten-parquet/stop @@ -0,0 +1 @@ +../keyten/stop \ No newline at end of file diff --git a/keyten-parquet/template.json b/keyten-parquet/template.json new file mode 100644 index 0000000000..25f746d14d --- /dev/null +++ b/keyten-parquet/template.json @@ -0,0 +1,7 @@ +{ + "system": "Keyten (Parquet)", + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust", "column-oriented", "dataframe", "stateless"] +} diff --git a/keyten/.gitignore b/keyten/.gitignore new file mode 100644 index 0000000000..70a523130d --- /dev/null +++ b/keyten/.gitignore @@ -0,0 +1,7 @@ +myenv/ +__pycache__/ +hits.parquet +hits.k10dir/ +server.pid +server.log +result.csv diff --git a/keyten/README.md b/keyten/README.md new file mode 100644 index 0000000000..986ae350e7 --- /dev/null +++ b/keyten/README.md @@ -0,0 +1,20 @@ +# Keyten + +This entry uses Keyten's Python dataframe API and durable native columnar +storage. Run `./benchmark.sh` from this directory on Ubuntu 24.04 or newer. +The installer pins the engine version. Loading streams the single source +Parquet file into native storage without sorting or manual indexes. + +`queries.sql` contains the 43 dataframe expressions, one per line. Source +timestamps remain integer seconds and dates remain integer days since the +Unix epoch; expression helpers implement the SQL temporal operations. + +The loopback HTTP server shares the engine's worker pool across requests. +The client times the complete request and response, including query planning, +execution, and JSON serialization of every output value. No query results +are cached. The standard driver restarts the server and clears the OS page +cache before each query's first execution, then runs two hot executions. +The health endpoint does not open the dataset. + +`data-size` reports all native storage files, including statistics and indexes. +The original downloaded Parquet file is not part of the stored database. diff --git a/keyten/benchmark.sh b/keyten/benchmark.sh new file mode 100755 index 0000000000..d6617aecd1 --- /dev/null +++ b/keyten/benchmark.sh @@ -0,0 +1,5 @@ +#!/bin/bash +export BENCH_DOWNLOAD_SCRIPT=download-hits-parquet-single +export BENCH_RESTARTABLE=yes +export BENCH_DURABLE=yes +exec ../lib/benchmark-common.sh diff --git a/keyten/check b/keyten/check new file mode 100755 index 0000000000..ffe494ecb7 --- /dev/null +++ b/keyten/check @@ -0,0 +1,3 @@ +#!/bin/bash +set -eu +curl --fail --silent "http://127.0.0.1:${BENCH_KEYTEN_PORT:-8000}/health" >/dev/null diff --git a/keyten/data-size b/keyten/data-size new file mode 100755 index 0000000000..b73afb69b5 --- /dev/null +++ b/keyten/data-size @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu +python3 - <<'PY' +import os +from pathlib import Path +root = Path(os.environ.get("KEYTEN_NATIVE", "hits.k10dir")) +print(sum(path.stat().st_size for path in root.rglob("*") if path.is_file())) +PY diff --git a/keyten/install b/keyten/install new file mode 100755 index 0000000000..9c409fb84c --- /dev/null +++ b/keyten/install @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu +sudo apt-get update -y +sudo apt-get install -y python3-pip python3-venv curl +python3 -m venv myenv +myenv/bin/pip install keyten==0.1.53 diff --git a/keyten/load b/keyten/load new file mode 100755 index 0000000000..71c4d4846c --- /dev/null +++ b/keyten/load @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu +myenv/bin/python - <<'PY' +import os +import keyten as kt + +kt.scan_parquet("hits.parquet").collect_to_native(os.environ.get("KEYTEN_NATIVE", "hits.k10dir")) +PY diff --git a/keyten/queries.sql b/keyten/queries.sql new file mode 100644 index 0000000000..dfe347d47d --- /dev/null +++ b/keyten/queries.sql @@ -0,0 +1,43 @@ +hits.select([kt.col('WatchID').count().alias('n')]).collect() +hits.filter(kt.col('AdvEngineID') != kt.lit(0)).select([kt.col('AdvEngineID').count().alias('n')]).collect() +hits.select([kt.col('AdvEngineID').sum().alias('a_sum'), kt.col('AdvEngineID').count().alias('count'), kt.col('ResolutionWidth').mean().alias('a_mean')]).collect() +hits.select([kt.col('UserID').mean().alias('m')]).collect() +hits.select([kt.col('UserID').n_unique().alias('u')]).collect() +hits.select([kt.col('SearchPhrase').n_unique().alias('u')]).collect() +hits.select([kt.col('EventDate').min().alias('e_min'), kt.col('EventDate').max().alias('e_max')]).collect() +hits.filter(kt.col('AdvEngineID') != kt.lit(0)).group_by([kt.col('AdvEngineID')]).agg([kt.col('AdvEngineID').count().alias('count')]).sort(['count'], descending=True).collect() +hits.group_by([kt.col('RegionID')]).agg([kt.col('UserID').n_unique().alias('u')]).sort(['u'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('RegionID')]).agg([kt.col('AdvEngineID').sum().alias('AdvEngineID_sum'), kt.col('AdvEngineID').count().alias('count'), kt.col('ResolutionWidth').mean().alias('ResolutionWidth_mean'), kt.col('UserID').n_unique().alias('UserID_nunique')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('MobilePhoneModel') != kt.lit('')).group_by([kt.col('MobilePhoneModel')]).agg([kt.col('UserID').n_unique().alias('UserID')]).sort(['UserID'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('MobilePhoneModel') != kt.lit('')).group_by([kt.col('MobilePhone'), kt.col('MobilePhoneModel')]).agg([kt.col('UserID').n_unique().alias('UserID')]).sort(['UserID'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).group_by([kt.col('SearchPhrase')]).agg([kt.col('SearchPhrase').count().alias('count')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).group_by([kt.col('SearchPhrase')]).agg([kt.col('UserID').n_unique().alias('UserID')]).sort(['UserID'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).group_by([kt.col('SearchEngineID'), kt.col('SearchPhrase')]).agg([kt.col('SearchPhrase').count().alias('count')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('UserID')]).agg([kt.col('UserID').count().alias('count')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('UserID'), kt.col('SearchPhrase')]).agg([kt.col('UserID').count().alias('count')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('UserID'), kt.col('SearchPhrase')]).agg([kt.col('UserID').count().alias('count')]).slice(0, 10).collect() +hits.group_by([kt.col('UserID'), minute('EventTime').alias('m'), kt.col('SearchPhrase')]).agg([kt.col('UserID').count().alias('count')]).sort(['count'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('UserID') == kt.lit(435090932899640449)).select([kt.col('UserID')]).collect() +hits.filter(kt.col('URL').str_contains('google')).select([kt.col('URL').count().alias('n')]).collect() +hits.filter(kt.col('URL').str_contains('google') & (kt.col('SearchPhrase') != kt.lit(''))).group_by([kt.col('SearchPhrase')]).agg([kt.col('URL').min().alias('URL'), kt.col('SearchPhrase').count().alias('count')]).select(['SearchPhrase', 'URL', 'count']).sort(['count'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('Title').str_contains('Google') & ~kt.col('URL').str_contains('.google.') & (kt.col('SearchPhrase') != kt.lit(''))).group_by([kt.col('SearchPhrase')]).agg([kt.col('URL').min().alias('URL'), kt.col('Title').min().alias('Title'), kt.col('SearchPhrase').count().alias('count'), kt.col('UserID').n_unique().alias('UserID')]).select(['SearchPhrase', 'URL', 'Title', 'count', 'UserID']).sort(['count'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('URL').str_contains('google')).sort(['EventTime']).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).sort(['EventTime']).select([kt.col('SearchPhrase')]).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).sort(['SearchPhrase']).select([kt.col('SearchPhrase')]).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).sort(['EventTime', 'SearchPhrase']).select([kt.col('SearchPhrase')]).slice(0, 10).collect() +hits.filter(kt.col('URL') != kt.lit('')).group_by([kt.col('CounterID')]).agg([kt.col('URL').str_len_bytes().mean().alias('l'), kt.col('URL').count().alias('c')]).filter(kt.col('c') > kt.lit(100000)).sort(['l'], descending=True).slice(0, 25).collect() +hits.filter(kt.col('Referer') != kt.lit('')).with_columns([domain('Referer').alias('k')]).group_by([kt.col('k')]).agg([kt.col('Referer').str_len_bytes().mean().alias('l'), kt.col('Referer').min().alias('min_referer'), kt.col('Referer').count().alias('c')]).filter(kt.col('c') > kt.lit(100000)).select(['k', 'l', 'c', 'min_referer']).sort(['l'], descending=True).slice(0, 25).collect() +hits.select([(kt.col('ResolutionWidth') + kt.lit(i)).sum().alias(f'c_{i}') for i in range(90)]).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).group_by([kt.col('SearchEngineID'), kt.col('ClientIP')]).agg([kt.col('ClientIP').count().alias('c'), kt.col('IsRefresh').sum().alias('IsRefreshSum'), kt.col('ResolutionWidth').mean().alias('AvgResolutionWidth')]).sort(['c'], descending=True).slice(0, 10).collect() +hits.filter(kt.col('SearchPhrase') != kt.lit('')).group_by([kt.col('WatchID'), kt.col('ClientIP')]).agg([kt.col('ClientIP').count().alias('c'), kt.col('IsRefresh').sum().alias('IsRefreshSum'), kt.col('ResolutionWidth').mean().alias('AvgResolutionWidth')]).sort(['c'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('WatchID'), kt.col('ClientIP')]).agg([kt.col('ClientIP').count().alias('c'), kt.col('IsRefresh').sum().alias('IsRefreshSum'), kt.col('ResolutionWidth').mean().alias('AvgResolutionWidth')]).sort(['c'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('URL')]).agg([kt.col('URL').count().alias('c')]).sort(['c'], descending=True).slice(0, 10).collect() +hits.group_by([kt.col('URL')]).agg([kt.col('URL').count().alias('c')]).sort(['c'], descending=True).with_columns([kt.lit(1).alias('one')]).select([kt.col('one'), kt.col('URL'), kt.col('c')]).slice(0, 10).collect() +hits.group_by([kt.col('ClientIP')]).agg([kt.col('ClientIP').count().alias('c')]).with_columns([(kt.col('ClientIP') - kt.lit(1)).alias('ClientIP_minus_1'), (kt.col('ClientIP') - kt.lit(2)).alias('ClientIP_minus_2'), (kt.col('ClientIP') - kt.lit(3)).alias('ClientIP_minus_3')]).select([kt.col('ClientIP'), kt.col('ClientIP_minus_1'), kt.col('ClientIP_minus_2'), kt.col('ClientIP_minus_3'), kt.col('c')]).sort(['c'], descending=True).slice(0, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('DontCountHits') == kt.lit(0)) & (kt.col('IsRefresh') == kt.lit(0)) & (kt.col('URL') != kt.lit(''))).group_by([kt.col('URL')]).agg([kt.col('URL').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(0, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('DontCountHits') == kt.lit(0)) & (kt.col('IsRefresh') == kt.lit(0)) & (kt.col('Title') != kt.lit(''))).group_by([kt.col('Title')]).agg([kt.col('Title').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(0, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('IsRefresh') == kt.lit(0)) & (kt.col('IsLink') != kt.lit(0)) & (kt.col('IsDownload') == kt.lit(0))).group_by([kt.col('URL')]).agg([kt.col('URL').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(1000, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('IsRefresh') == kt.lit(0))).with_columns([kt.if_else((kt.col('SearchEngineID') == kt.lit(0)) & (kt.col('AdvEngineID') == kt.lit(0)), kt.col('Referer'), kt.lit('')).alias('Src')]).group_by([kt.col('TraficSourceID'), kt.col('SearchEngineID'), kt.col('AdvEngineID'), kt.col('Src'), kt.col('URL')]).agg([kt.col('URL').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(1000, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('IsRefresh') == kt.lit(0)) & kt.col('TraficSourceID').is_in([-1, 6]) & (kt.col('RefererHash') == kt.lit(3594120000172545465))).group_by([kt.col('URLHash'), kt.col('EventDate')]).agg([kt.col('URLHash').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(100, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 1)) & (kt.col('EventDate') <= date(2013, 7, 31)) & (kt.col('IsRefresh') == kt.lit(0)) & (kt.col('DontCountHits') == kt.lit(0)) & (kt.col('URLHash') == kt.lit(2868770270353813622))).group_by([kt.col('WindowClientWidth'), kt.col('WindowClientHeight')]).agg([kt.col('URLHash').count().alias('PageViews')]).sort(['PageViews'], descending=True).slice(10000, 10).collect() +hits.filter((kt.col('CounterID') == kt.lit(62)) & (kt.col('EventDate') >= date(2013, 7, 14)) & (kt.col('EventDate') <= date(2013, 7, 15)) & (kt.col('IsRefresh') == kt.lit(0)) & (kt.col('DontCountHits') == kt.lit(0))).group_by([minute_trunc('EventTime').alias('M')]).agg([kt.col('CounterID').count().alias('PageViews')]).sort(['M']).slice(1000, 10).collect() diff --git a/keyten/query b/keyten/query new file mode 100755 index 0000000000..cf051c8a90 --- /dev/null +++ b/keyten/query @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import os +import sys +import time +import urllib.error +import urllib.request + +code = sys.stdin.buffer.read() +port = os.environ.get("BENCH_KEYTEN_PORT", "8000") +request = urllib.request.Request(f"http://127.0.0.1:{port}/query", data=code) +start = time.perf_counter() +try: + with urllib.request.urlopen(request, timeout=1800) as response: + body = response.read() +except urllib.error.HTTPError as error: + sys.stderr.buffer.write(error.read() + b"\n") + sys.exit(1) +elapsed = time.perf_counter() - start +sys.stdout.buffer.write(body + b"\n") +print(f"{elapsed:.9f}", file=sys.stderr) diff --git a/keyten/results/20261002/c6a.2xlarge.json b/keyten/results/20261002/c6a.2xlarge.json new file mode 100644 index 0000000000..7d0e32f529 --- /dev/null +++ b/keyten/results/20261002/c6a.2xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten", + "date": "2026-10-02", + "machine": "c6a.2xlarge", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","embedded"], + "load_time": 1342, + "data_size": 33661618867, + "concurrent_qps": 0.038, + "concurrent_error_ratio": 0.042, + "result": [ + [5.261, 0.029, 0.029], + [0.201, 0.044, 0.047], + [0.7, 0.059, 0.059], + [5.548, 0.075, 0.075], + [6.61, 1.068, 1.125], + [17.689, 1.038, 1.015], + [0.158, 0.032, 0.032], + [0.224, 0.048, 0.047], + [7.349, 1.172, 1.19], + [8.142, 1.349, 1.348], + [6.246, 0.242, 0.243], + [6.426, 0.259, 0.266], + [17.372, 0.707, 0.708], + [24.505, 2.427, 2.376], + [17.83, 0.826, 0.809], + [6.803, 1.06, 1.021], + [24.338, 1.94, 1.893], + [26.768, 4.141, 6.122], + [26.49, 3.002, 2.817], + [5.513, 0.022, 0.022], + [43.425, 0.298, 0.303], + [59.257, 0.514, 0.514], + [106.034, 0.708, 0.707], + [217.697, 35.158, 0.852], + [17.963, 0.312, 0.312], + [16.928, 0.327, 0.326], + [17.999, 0.34, 0.34], + [43.668, 0.771, 0.742], + [46.06, 9.084, 9.074], + [0.658, 0.047, 0.054], + [25.851, 4.913, 4.922], + [33.835, 8.223, 8.316], + [null, null, null], + [45.032, 2.407, 2.406], + [44.473, 2.42, 2.397], + [4.574, 1.198, 1.152], + [36.697, 0.099, 0.099], + [36.268, 0.059, 0.055], + [36.768, 0.045, 0.045], + [74.885, 0.97, 0.887], + [15.562, 0.037, 0.038], + [9.297, 0.034, 0.034], + [1.484, 0.044, 0.049] +] + } + \ No newline at end of file diff --git a/keyten/results/20261002/c6a.xlarge.json b/keyten/results/20261002/c6a.xlarge.json new file mode 100644 index 0000000000..a75ac671f7 --- /dev/null +++ b/keyten/results/20261002/c6a.xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten", + "date": "2026-10-02", + "machine": "c6a.xlarge", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","embedded"], + "load_time": 1910, + "data_size": 33661618867, + "concurrent_qps": 0.02, + "concurrent_error_ratio": 0.077, + "result": [ + [5.144, 0.032, 0.032], + [0.217, 0.058, 0.058], + [0.736, 0.094, 0.096], + [5.437, 0.115, 0.114], + [7.576, 2.243, 2.24], + [18.289, 2.018, 2.004], + [0.169, 0.036, 0.037], + [0.256, 0.073, 0.081], + [8.22, 2.136, 2.128], + [9.136, 2.551, 2.571], + [6.429, 0.558, 0.55], + [6.813, 0.696, 0.677], + [25.205, 8.825, 8.936], + [32.142, 10.48, 30.697], + [26.462, 9.65, 9.965], + [14.978, 9.694, 9.803], + [75.007, 315.867, 225.685], + [76.164, 105.208, 277.123], + [null, null, null], + [5.045, 0.021, 0.022], + [42.458, 0.517, 0.513], + [58.719, 1.521, 1.008], + [128.294, 42.908, 41.438], + [214.334, 26.303, 17.536], + [17.866, 0.594, 0.596], + [16.71, 0.624, 0.624], + [17.814, 0.653, 0.65], + [213.923, 309.164, 255.572], + [502.37, 742.459, 814.452], + [0.644, 0.065, 0.065], + [25.955, 5.621, 5.692], + [34.495, 9.529, 12.605], + [null, null, null], + [944.653, null, null], + [1340.283, null, null], + [9.414, 6.523, 6.568], + [36.021, 0.394, 0.428], + [35.351, 0.127, 0.124], + [35.854, 0.063, 0.061], + [73.163, 1.084, 1.08], + [15.109, 0.047, 0.047], + [9.104, 0.037, 0.037], + [1.464, 0.056, 0.057] +] + } + \ No newline at end of file diff --git a/keyten/results/20261003/c6a.large.json b/keyten/results/20261003/c6a.large.json new file mode 100644 index 0000000000..b672565802 --- /dev/null +++ b/keyten/results/20261003/c6a.large.json @@ -0,0 +1,60 @@ +{ + "system": "Keyten", + "date": "2026-10-03", + "machine": "c6a.large", + "cluster_size": 1, + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","dataframe","embedded"], + "load_time": 2686, + "data_size": 33661618867, + "concurrent_qps": 0.023, + "concurrent_error_ratio": 0.125, + "result": [ + [5.304, 0.041, 0.042], + [0.253, 0.086, 0.085], + [0.84, 0.162, 0.162], + [5.694, 0.203, 0.203], + [11.05, 5.45, 5.403], + [20.698, 3.926, 3.915], + [0.182, 0.048, 0.048], + [0.281, 0.102, 0.102], + [10.342, 4.154, 4.093], + [11.655, 4.899, 4.969], + [6.782, 0.806, 0.794], + [7.279, 1.011, 0.988], + [26.873, 56.912, 54.837], + [80.59, 191.336, 310.227], + [38.851, 68.353, 178.53], + [16.483, 87.168, 96.622], + [552.6, 938.775, 911.439], + [753.919, 1465.199, 1573.695], + [null, null, null], + [5.694, 0.021, 0.021], + [88.934, 22.31, 21.628], + [115.776, 26.805, 24.918], + [190.928, 47.158, 44.86], + [294.151, 42.587, 43.685], + [19.366, 1.16, 1.163], + [18.004, 1.208, 1.21], + [19.249, 1.273, 1.286], + [284.991, 284.409, 271.737], + [482.765, 1103.32, 1153.466], + [0.706, 0.106, 0.106], + [28.315, 11.526, 67.836], + [57.047, 195.371, 315.839], + [null, null, null], + [null, null, null], + [null, null, null], + [9.963, 6.312, 7.317], + [76.446, 0.64, 0.441], + [72.847, 0.277, 0.145], + [75.122, 0.208, 0.078], + [119.487, 1.268, 1.024], + [15.726, 0.048, 0.048], + [9.885, 0.039, 0.038], + [1.593, 0.071, 0.071] +] + } + \ No newline at end of file diff --git a/keyten/server.py b/keyten/server.py new file mode 100644 index 0000000000..4d7703378e --- /dev/null +++ b/keyten/server.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Loopback-only dataframe benchmark server; queries are trusted Python expressions.""" + +import json +import os +import threading +from datetime import date as pydate +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +import keyten as kt + +STORE = Path(os.environ.get("KEYTEN_NATIVE", "hits.k10dir")) +PARQUET = os.environ.get("KEYTEN_PARQUET") +PORT = int(os.environ.get("BENCH_KEYTEN_PORT", "8000")) +EPOCH = pydate(1970, 1, 1) +hits = None +open_lock = threading.Lock() + + +def date(y, m, d): + return kt.lit((pydate(y, m, d) - EPOCH).days) + + +def minute(name): + t = kt.col(name) + return ((t % 3600 - t % 60) / 60).cast("int") + + +def minute_trunc(name): + t = kt.col(name) + return t - t % 60 + + +def domain(name): + value = kt.col(name) + extracted = value.str_extract(r"^https?://(?:www\.)?([^/]+)/.*$") + return kt.if_else(extracted.is_not_null(), extracted, value) + + +def execute(code): + global hits + # Opening belongs to the first timed request, so health checks cannot + # populate source metadata or data caches before a cold query. + if hits is None: + with open_lock: + if hits is None: + hits = kt.scan_parquet(PARQUET) if PARQUET else kt.scan_native(str(STORE)) + result = eval(compile(code, "", "eval"), { + "kt": kt, "hits": hits, "date": date, + "minute": minute, "minute_trunc": minute_trunc, "domain": domain, + }) + # Complete column values, including every row and string, cross the wire. + # Never use the dataframe's abbreviated display representation. + return result.to_dict() + + +class Handler(BaseHTTPRequestHandler): + def respond(self, status, value): + body = json.dumps(value, ensure_ascii=False, allow_nan=False).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path == "/health": + self.respond(200, {"ok": True, "version": kt.__version__}) + else: + self.respond(404, {"error": "unknown route"}) + + def do_POST(self): + if self.path != "/query": + self.respond(404, {"error": "unknown route"}) + return + try: + code = self.rfile.read(int(self.headers["Content-Length"])).decode() + result = execute(code) + self.respond(200, result) + except Exception as error: + self.respond(500, {"error": f"{type(error).__name__}: {error}"}) + + def log_message(self, *args): + pass + + +if __name__ == "__main__": + ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever() diff --git a/keyten/start b/keyten/start new file mode 100755 index 0000000000..df51fdd348 --- /dev/null +++ b/keyten/start @@ -0,0 +1,7 @@ +#!/bin/bash +set -eu +if [ -f server.pid ] && kill -0 "$(cat server.pid)" 2>/dev/null; then + exit 0 +fi +nohup myenv/bin/python "${KEYTEN_SERVER_SCRIPT:-server.py}" >server.log 2>&1 & +echo $! >server.pid diff --git a/keyten/stop b/keyten/stop new file mode 100755 index 0000000000..e60837b4d3 --- /dev/null +++ b/keyten/stop @@ -0,0 +1,14 @@ +#!/bin/bash +set -eu +if [ -f server.pid ]; then + pid=$(cat server.pid) + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + for _ in $(seq 1 10); do + if ! kill -0 "$pid" 2>/dev/null; then break; fi + sleep 1 + done + kill -9 "$pid" 2>/dev/null || true + fi + rm -f server.pid +fi diff --git a/keyten/template.json b/keyten/template.json new file mode 100644 index 0000000000..c108dc30b5 --- /dev/null +++ b/keyten/template.json @@ -0,0 +1,7 @@ +{ + "system": "Keyten", + "proprietary": "yes", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust", "column-oriented", "dataframe", "embedded"] +}