From c46824fa1dd6391abe433f2fb439a7e2d88f0b83 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Fri, 14 Aug 2026 19:37:01 +0000 Subject: [PATCH 1/2] Add Rayforce Rayforce (https://github.com/RayforceDB/rayforce) is a zero-dependency columnar analytics and graph engine in pure C, queried through Rayfall, its Lisp-like language. queries.sql therefore holds 43 Rayfall expressions, one per line, the way jd/bqn hold their own dialects. (Not to be confused with the closed #940, which was RayforceDB's internal 10M-row harness opened against this repo by mistake.) Two measured engine properties shaped the entry: - Text columns are loaded as SYM (dictionary-encoded), not STR. STR pool offsets are uint32_t, so a column's pool is capped at 4 GiB, and at 100M rows URL/Title/Referer/OriginalURL need 5-11 GB each. Opening a splayed table also validates every STR element, and that cost is superlinear: 0.4 s at 2M rows, 199 s at 9.6M rows (99 s for the parted layout). All-SYM opens in 17 s at 9.6M rows and stores the same subset in 4.4 GB instead of 7.5 GB. - It runs as an IPC server, with BENCH_RESTARTABLE=yes. Opening the table is eager (validate every column file, load the dictionary), so a CLI-per-query would pay that 129 times per run; the server pays it once per start and times queries with `timeit` server-side. The restart is not optional either: with the server left up, drop_caches cannot evict pages a live process still maps, and a "cold" query measured 0.029 s against 0.44 s after a real restart, so keeping the process alive would report every run as warm. BENCH_CHECK_TIMEOUT is raised to 1800 s because the readiness probe now waits for that table open. Query notes: HAVING becomes an outer select over the grouped one; LIMIT/OFFSET is take: [m n]; DATE_TRUNC('minute', EventTime) is (xbar EventTime 60000000000); CASE WHEN is the row-wise (if ...); Q29's REGEXP_REPLACE is rebuilt from str-find/substr/if and reproduces the regex exactly on this dataset, fallback rows included. Q25/Q27 project EventTime alongside SearchPhrase because sorting runs after projection, and Q35 projects the constant group key in an outer select. All 43 queries were checked against clickhouse-local on a 2M-row subset of the same CSV: 33 match exactly or modulo row order, and the other 10 differ only in which of several equally-ranked rows a LIMIT over ties returns (the sort-key multisets are identical). install, load, the stop/drop_caches/start/check cold cycle, 43 queries through ./query, 10 concurrent clients and the error paths were all exercised end to end. Two upstream bugs found on the way, neither affecting this entry: RayforceDB/rayforce#404 (take: drops pool-backed STR values) and RayforceDB/rayforce#405 (ungrouped count(distinct) in a select projection returns the row count). Co-Authored-By: Claude Opus 5 (1M context) --- rayforce/.gitignore | 2 + rayforce/README.md | 144 +++++++++++++++++++++++++++++++++++++++++ rayforce/benchmark.sh | 11 ++++ rayforce/check | 4 ++ rayforce/check.rfl | 8 +++ rayforce/create.rfl | 42 ++++++++++++ rayforce/data-size | 5 ++ rayforce/install | 36 +++++++++++ rayforce/load | 15 +++++ rayforce/load.rfl | 9 +++ rayforce/queries.sql | 43 ++++++++++++ rayforce/query | 27 ++++++++ rayforce/query.rfl | 13 ++++ rayforce/server.rfl | 18 ++++++ rayforce/start | 17 +++++ rayforce/stop | 13 ++++ rayforce/template.json | 11 ++++ 17 files changed, 418 insertions(+) create mode 100644 rayforce/.gitignore create mode 100644 rayforce/README.md create mode 100755 rayforce/benchmark.sh create mode 100755 rayforce/check create mode 100644 rayforce/check.rfl create mode 100644 rayforce/create.rfl create mode 100755 rayforce/data-size create mode 100755 rayforce/install create mode 100755 rayforce/load create mode 100644 rayforce/load.rfl create mode 100644 rayforce/queries.sql create mode 100755 rayforce/query create mode 100644 rayforce/query.rfl create mode 100644 rayforce/server.rfl create mode 100755 rayforce/start create mode 100755 rayforce/stop create mode 100644 rayforce/template.json diff --git a/rayforce/.gitignore b/rayforce/.gitignore new file mode 100644 index 0000000000..a3c0980381 --- /dev/null +++ b/rayforce/.gitignore @@ -0,0 +1,2 @@ +# Splayed table written by ./load (one file per column plus .sym). +hits/ diff --git a/rayforce/README.md b/rayforce/README.md new file mode 100644 index 0000000000..be5eecc40c --- /dev/null +++ b/rayforce/README.md @@ -0,0 +1,144 @@ +# Rayforce + +[Rayforce](https://github.com/RayforceDB/rayforce) is a zero-dependency +columnar analytics and graph engine written in pure C (MIT). Columnar +scans and graph traversals share one operation DAG, which is optimized +and then executed as morsel-driven bytecode over 1024-row batches. +Queries are written in **Rayfall**, Rayforce's Lisp-like query language, +so `queries.sql` holds 43 Rayfall expressions (one per line) rather than +SQL. + +## Install + +`./install` builds from source with `make release`. Rayforce publishes an +amd64 `.deb` and prebuilt tarballs, but ClickBench also runs on aarch64 +machines and the engine is dependency-free C that builds with a plain +`make` on both, so building is the only setup that covers every machine +in the matrix. `RAYFORCE_VERSION=vX.Y.Z ./install` pins a release; +unset, it takes the latest GitHub release. + +## Data layout + +`./load` streams `hits.csv` into a splayed on-disk table under `./hits` +with `.csv.splayed` — one file per column plus the `.sym` dictionary, +parsed in parallel from an mmap of the CSV without materializing the +table in memory. The column names come from `create.rfl`; passing an +explicit name vector also tells the reader the input has no header row, +which is the shape of the published `hits.csv`. + +Types follow `../clickhouse/create.sql`: `BIGINT` → `I64`, `INTEGER` → +`I32`, `SMALLINT` → `I16`, `TIMESTAMP` → `TIMESTAMP`, `Date` → `DATE`. +Rayforce's CSV reader parses the `YYYY-MM-DD` and `YYYY-MM-DD HH:MM:SS` +forms in the file into its native date/timestamp types, so +`EventDate >= '2013-07-01'`, `extract(minute FROM EventTime)` and +`DATE_TRUNC('minute', EventTime)` all work on native values. + +### Why every text column is SYM + +Rayforce has two text column types: `SYM` (dictionary-encoded, one +integer index per row into a global intern table) and `STR` +(variable-length, 12 bytes inline or a per-vector byte pool). `STR` is +the type its docs recommend for high-cardinality text such as URLs, but +it does not reach ClickBench scale in the current release: + +* **The pool offset is a `uint32_t`**, so a column's pool is capped at + 4 GiB (`src/io/csv.c` bails out above it). At 100M rows the `URL`, + `Title`, `Referer` and `OriginalURL` columns hold roughly 9, 11, 8 and + 5 GB of bytes, so none of them fit in one `STR` column. +* **Opening a `STR` column validates every element** (bounds plus a + 4-byte prefix compare against the pool, `col_validate_str_region`), + and the cost is superlinear: on a subset of this dataset a splayed + table with five `STR` columns opened in 0.4 s at 2M rows but 199 s at + 9.6M rows. The same table with dictionary-encoded text opened in + 17 s at 9.6M rows. + +Loading all TEXT / VARCHAR / CHAR columns as `SYM` sidesteps both: it +has no 4 GiB limit, and the columns become narrow integer vectors (the +9.6M-row subset is 4.4 GB with every text column dictionary-encoded, +versus 7.5 GB with those five columns as `STR`). +Dictionary encoding is what makes the dataset loadable at all here; it +also makes `GROUP BY URL` an integer group-by, while the string +operations in Q28 and Q29 pay an extra indirection per row to resolve +symbol ids back to bytes. A third reason not to go back to `STR` for +now: `take:` — i.e. every `LIMIT` — currently returns empty strings for +pool-backed values +([RayforceDB/rayforce#404](https://github.com/RayforceDB/rayforce/issues/404)). + +## Server mode + +Rayforce is embeddable and normally invoked as a CLI, but this entry runs +it as an IPC server (`./start` → `rayforce -p 5000 server.rfl`) because +opening the table is eager: it validates every column file and loads the +symbol dictionary before the first query. Paying that once per server +start instead of once per query process keeps it out of the reported +numbers and out of the run's wall clock. The listening socket only +accepts connections after `server.rfl` finishes, so `./check` — a +one-expression IPC round trip — is a genuine readiness probe. + +`./query` sends the query text; the server evaluates +`(timeit (set rf-result ))`, which returns the elapsed +milliseconds from a nanosecond clock, and a second untimed round trip +pulls the result back so it can be printed. Timing therefore covers +server-side query execution only, the same convention as the other +embedded engines here (e.g. DuckDB's `.timer`). + +### Cold runs + +`BENCH_RESTARTABLE=yes` is required, not optional: with the server left +running, `drop_caches` cannot evict pages that a live process still has +mapped, and a "cold" query measured 0.029 s — exactly its warm time — +versus 0.44 s after a real restart. Restarting between queries makes the +cold number honest, at the price of re-reading the table on every start +(hence `BENCH_CHECK_TIMEOUT=1800`). Note the flip side: because the open +is eager, on a machine whose RAM comfortably exceeds the dataset the +first query still runs against a fully resident table. + +## Query adaptations + +The queries are direct Rayfall translations of the ClickBench SQL. All 43 +were checked against `clickhouse-local` on a 2M-row subset of the same +CSV; the only differences are which of several equally-ranked rows a +`LIMIT` over ties returns, plus the projection differences noted below. +Points worth knowing: + +* **`COUNT(*)`** is `(count hits)`; `COUNT(*)` over a filter is + `(count (select {...}))`. +* **Ungrouped `COUNT(DISTINCT c)`** (Q5, Q6) is a whole-column reducer, + `(count (distinct (at hits 'c)))`, and not a `select` projection: the + projection form returns the row count instead of the distinct count + ([RayforceDB/rayforce#405](https://github.com/RayforceDB/rayforce/issues/405)). + Per-group `COUNT(DISTINCT c)` (Q9-Q12, Q14, Q23) is written inline as + `(count (distinct UserID))` and lowers to Rayforce's grouped + count-distinct kernels. +* **`HAVING`** (Q28, Q29) has no clause form: the group-by runs as an + inner `select` and the outer one filters on the aggregate. +* **`LIMIT n OFFSET m`** (Q39-Q43) is `take: [m n]`. +* **`extract(minute FROM EventTime)`** (Q19) is `(minute EventTime)`; + **`DATE_TRUNC('minute', EventTime)`** (Q43) is + `(xbar EventTime 60000000000)`, i.e. truncation to whole minutes of + the nanosecond timestamp. +* **`CASE WHEN ... END`** (Q40) is the row-wise `(if cond then else)`, + which the query compiler lowers to the DAG's ternary select. +* **`ORDER BY` a column that is not selected** (Q25, Q27): sorting + happens after projection, so those queries project `EventTime` + alongside `SearchPhrase`. Same rows, same order, one extra column in + the printed output. +* **`GROUP BY 1, URL`** (Q35): a constant is not accepted as a group + key, and grouping by `(1, URL)` is the same partition as grouping by + `URL`, so the constant is projected by an outer `select` instead. +* **Derived group keys** (Q36) go in the `by:` dict as + `ip1: (- ClientIP 1)`, which the optimizer turns into a synthetic + column rather than a materialized one. Rayforce emits the aggregate + before the derived keys, so the result has the same rows as the SQL + with the columns in a different order. +* **`REGEXP_REPLACE`** (Q29): Rayforce has no regex engine, so the + host extraction `^https?://(?:www\.)?([^/]+)/.*$` is spelled out with + the string builtins — `str-find` for `://`, `www.` and the first `/`, + `substr` to cut, and `if` to fall back to the whole `Referer` when the pattern + does not match (no `http`/`https` scheme at offset 0, or no `/` after + the host). On this dataset it reproduces `REGEXP_REPLACE` exactly, + including the fallback rows: the group keys, counts, `MIN(Referer)` + and average lengths all match ClickHouse. +* **`MIN(URL)` / `MIN(Title)`** (Q22, Q23) work directly on `SYM` + columns and return the lexicographic minimum, not the minimum + dictionary index. diff --git a/rayforce/benchmark.sh b/rayforce/benchmark.sh new file mode 100755 index 0000000000..ba36b0e160 --- /dev/null +++ b/rayforce/benchmark.sh @@ -0,0 +1,11 @@ +#!/bin/bash +export BENCH_DOWNLOAD_SCRIPT="download-hits-csv" +# Rayforce runs as an IPC server here (./start), so the stop/drop_caches/start +# cold cycle is meaningful and the concurrent-QPS test hits a shared process. +export BENCH_RESTARTABLE=yes +export BENCH_DURABLE=yes +# Opening the splayed table validates every column file and loads the symbol +# dictionary; at 100M rows that takes minutes, and it happens on every restart, +# so the readiness probe needs a much longer window than the 300s default. +export BENCH_CHECK_TIMEOUT="${BENCH_CHECK_TIMEOUT:-1800}" +exec ../lib/benchmark-common.sh diff --git a/rayforce/check b/rayforce/check new file mode 100755 index 0000000000..67948aa8fc --- /dev/null +++ b/rayforce/check @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +export RAYFORCE_PORT="${RAYFORCE_PORT:-5000}" +rayforce check.rfl >/dev/null diff --git a/rayforce/check.rfl b/rayforce/check.rfl new file mode 100644 index 0000000000..8247214941 --- /dev/null +++ b/rayforce/check.rfl @@ -0,0 +1,8 @@ +; Readiness probe: connect over IPC and evaluate a trivial expression. +; The server binds its socket before running server.rfl but only accepts +; connections once that script has finished opening the table, so a successful +; round trip means "table open, ready to query". +(set h (.ipc.open (format "127.0.0.1:%" (.os.getenv "RAYFORCE_PORT")) 2000)) +(set ok (.ipc.send h "(+ 1 1)")) +(.ipc.close h) +(if (== ok 2) (exit 0) (exit 1)) diff --git a/rayforce/create.rfl b/rayforce/create.rfl new file mode 100644 index 0000000000..55befb5a4c --- /dev/null +++ b/rayforce/create.rfl @@ -0,0 +1,42 @@ +; ClickBench `hits` schema for Rayforce. +; +; `load` feeds these two vectors to `.csv.splayed`, which streams +; hits.csv straight into the on-disk splayed table without a header row +; (an explicit name vector implies headerless input). +; +; Types follow ../clickhouse/create.sql: BIGINT -> I64, INTEGER -> I32, +; SMALLINT -> I16, TIMESTAMP -> TIMESTAMP, Date -> DATE. Every TEXT / +; VARCHAR / CHAR column is loaded as SYM (dictionary-encoded) rather +; than STR -- see README.md "Why every text column is SYM". + +(set hits-names [ + WatchID JavaEnable Title GoodEvent EventTime EventDate CounterID ClientIP + RegionID UserID CounterClass OS UserAgent URL Referer IsRefresh + RefererCategoryID RefererRegionID URLCategoryID URLRegionID + ResolutionWidth ResolutionHeight ResolutionDepth FlashMajor FlashMinor + FlashMinor2 NetMajor NetMinor UserAgentMajor UserAgentMinor CookieEnable + JavascriptEnable IsMobile MobilePhone MobilePhoneModel Params IPNetworkID + TraficSourceID SearchEngineID SearchPhrase AdvEngineID IsArtifical + WindowClientWidth WindowClientHeight ClientTimeZone ClientEventTime + SilverlightVersion1 SilverlightVersion2 SilverlightVersion3 + SilverlightVersion4 PageCharset CodeVersion IsLink IsDownload IsNotBounce + FUniqID OriginalURL HID IsOldCounter IsEvent IsParameter DontCountHits + WithHash HitColor LocalEventTime Age Sex Income Interests Robotness + RemoteIP WindowName OpenerName HistoryLength BrowserLanguage + BrowserCountry SocialNetwork SocialAction HTTPError SendTiming DNSTiming + ConnectTiming ResponseStartTiming ResponseEndTiming FetchTiming + SocialSourceNetworkID SocialSourcePage ParamPrice ParamOrderID + ParamCurrency ParamCurrencyID OpenstatServiceName OpenstatCampaignID + OpenstatAdID OpenstatSourceID UTMSource UTMMedium UTMCampaign UTMContent + UTMTerm FromTag HasGCLID RefererHash URLHash CLID +]) + +(set hits-types [ + I64 I16 SYM I16 TIMESTAMP DATE I32 I32 I32 I64 I16 I16 I16 SYM SYM I16 + I16 I32 I16 I32 I16 I16 I16 I16 I16 SYM I16 I16 I16 SYM I16 I16 I16 I16 + SYM SYM I32 I16 I16 SYM I16 I16 I16 I16 I16 TIMESTAMP I16 I16 I32 I16 SYM + I32 I16 I16 I16 I64 SYM I32 I16 I16 I16 I16 I16 SYM TIMESTAMP I16 I16 I16 + I16 I16 I32 I32 I32 I16 SYM SYM SYM SYM I16 I32 I32 I32 I32 I32 I32 I16 + SYM I64 SYM SYM I16 SYM SYM SYM SYM SYM SYM SYM SYM SYM SYM I16 I64 I64 + I32 +]) diff --git a/rayforce/data-size b/rayforce/data-size new file mode 100755 index 0000000000..a7bc3a6540 --- /dev/null +++ b/rayforce/data-size @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +# Splayed table: one file per column plus the `.sym` dictionary, all under ./hits. +du -sb hits | cut -f1 diff --git a/rayforce/install b/rayforce/install new file mode 100755 index 0000000000..70f8b453b4 --- /dev/null +++ b/rayforce/install @@ -0,0 +1,36 @@ +#!/bin/bash +# Builds Rayforce from source. +# +# Rayforce publishes an amd64 .deb and prebuilt tarballs, but ClickBench also +# runs on aarch64 machines (c8g.*), and the engine is zero-dependency C that +# builds with a plain `make release` on both. Building from source is therefore +# the only setup that works everywhere, and it is what upstream documents first. +# +# RAYFORCE_VERSION pins the release tag; unset means "latest GitHub release" +# (falling back to a known-good tag when the unauthenticated API call is rate +# limited). +set -e + +RAYFORCE_FALLBACK_VERSION=v2.5.14 + +if ! command -v rayforce >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y build-essential git curl + + version="${RAYFORCE_VERSION:-}" + if [ -z "$version" ]; then + version=$(curl -sS --max-time 30 \ + https://api.github.com/repos/RayforceDB/rayforce/releases/latest \ + | grep -o '"tag_name": *"[^"]*"' | head -n1 | cut -d'"' -f4 || true) + [ -z "$version" ] && version="$RAYFORCE_FALLBACK_VERSION" + fi + echo "Building Rayforce $version" + + rm -rf "$HOME/rayforce-src" + git clone --depth 1 --branch "$version" \ + https://github.com/RayforceDB/rayforce "$HOME/rayforce-src" + make -C "$HOME/rayforce-src" release -j"$(nproc)" + sudo install -m 755 "$HOME/rayforce-src/rayforce" /usr/local/bin/rayforce +fi + +rayforce --help >/dev/null diff --git a/rayforce/load b/rayforce/load new file mode 100755 index 0000000000..ebfc03216c --- /dev/null +++ b/rayforce/load @@ -0,0 +1,15 @@ +#!/bin/bash +# Loads hits.csv into the splayed table ./hits. +# +# The load runs in its own rayforce process, not through the server: the server +# started by the driver before ./load has no table open yet and picks it up at +# the next restart, which happens before the first query. +set -e + +rm -rf hits + +rayforce load.rfl + +# The CSV is no longer needed once the columns are on disk. +rm -f hits.csv +sync diff --git a/rayforce/load.rfl b/rayforce/load.rfl new file mode 100644 index 0000000000..1003d0f102 --- /dev/null +++ b/rayforce/load.rfl @@ -0,0 +1,9 @@ +; Stream hits.csv into a splayed on-disk table under ./hits. +; +; `.csv.splayed` mmaps the CSV, parses chunks in parallel and writes one column +; file per field plus the `.sym` dictionary, without materialising the whole +; table in memory. The name vector implies the input has no header row, which is +; the shape of the published hits.csv. +(load "create.rfl") +(.csv.splayed hits-names hits-types "hits.csv" "hits") +(exit 0) diff --git a/rayforce/queries.sql b/rayforce/queries.sql new file mode 100644 index 0000000000..ca52104811 --- /dev/null +++ b/rayforce/queries.sql @@ -0,0 +1,43 @@ +(count hits) +(count (select {from: hits where: (!= AdvEngineID 0)})) +(select {from: hits sum_adv: (sum AdvEngineID) c: (count AdvEngineID) avg_res: (avg ResolutionWidth)}) +(select {from: hits avg_user: (avg UserID)}) +(count (distinct (at hits 'UserID))) +(count (distinct (at hits 'SearchPhrase))) +(select {from: hits min_date: (min EventDate) max_date: (max EventDate)}) +(select {from: hits by: AdvEngineID c: (count AdvEngineID) where: (!= AdvEngineID 0) desc: c}) +(select {from: hits by: RegionID u: (count (distinct UserID)) desc: u take: 10}) +(select {from: hits by: RegionID sum_adv: (sum AdvEngineID) c: (count AdvEngineID) avg_res: (avg ResolutionWidth) u: (count (distinct UserID)) desc: c take: 10}) +(select {from: hits by: MobilePhoneModel u: (count (distinct UserID)) where: (!= MobilePhoneModel "") desc: u take: 10}) +(select {from: hits by: [MobilePhone MobilePhoneModel] u: (count (distinct UserID)) where: (!= MobilePhoneModel "") desc: u take: 10}) +(select {from: hits by: SearchPhrase c: (count SearchPhrase) where: (!= SearchPhrase "") desc: c take: 10}) +(select {from: hits by: SearchPhrase u: (count (distinct UserID)) where: (!= SearchPhrase "") desc: u take: 10}) +(select {from: hits by: [SearchEngineID SearchPhrase] c: (count SearchPhrase) where: (!= SearchPhrase "") desc: c take: 10}) +(select {from: hits by: UserID c: (count UserID) desc: c take: 10}) +(select {from: hits by: [UserID SearchPhrase] c: (count UserID) desc: c take: 10}) +(select {from: hits by: [UserID SearchPhrase] c: (count UserID) take: 10}) +(select {from: hits by: {UserID: UserID m: (minute EventTime) SearchPhrase: SearchPhrase} c: (count UserID) desc: c take: 10}) +(select {from: hits UserID: UserID where: (== UserID 435090932899640449)}) +(count (select {from: hits where: (like URL "*google*")})) +(select {from: hits by: SearchPhrase min_url: (min URL) c: (count URL) where: (and (like URL "*google*") (!= SearchPhrase "")) desc: c take: 10}) +(select {from: hits by: SearchPhrase min_url: (min URL) min_title: (min Title) c: (count URL) u: (count (distinct UserID)) where: (and (like Title "*Google*") (not (like URL "*.google.*")) (!= SearchPhrase "")) desc: c take: 10}) +(select {from: hits where: (like URL "*google*") asc: EventTime take: 10}) +(select {from: hits SearchPhrase: SearchPhrase EventTime: EventTime where: (!= SearchPhrase "") asc: EventTime take: 10}) +(select {from: hits SearchPhrase: SearchPhrase where: (!= SearchPhrase "") asc: SearchPhrase take: 10}) +(select {from: hits SearchPhrase: SearchPhrase EventTime: EventTime where: (!= SearchPhrase "") asc: [EventTime SearchPhrase] take: 10}) +(select {from: (select {from: hits by: CounterID l: (avg (strlen URL)) c: (count URL) where: (!= URL "")}) where: (> c 100000) desc: l take: 25}) +(xcol (select {from: (select {from: hits by: (let p (str-find Referer "://") (let s (substr Referer (+ p 4) -1) (let r (if (== (str-find s "www.") 0) (substr s 5 -1) s) (let sl (str-find r "/") (if (and (within p [4 5]) (== (substr Referer 1 4) "http") (not (nil? sl))) (substr r 1 sl) Referer))))) l: (avg (strlen Referer)) c: (count Referer) min_ref: (min Referer) where: (!= Referer "")}) where: (> c 100000) desc: l take: 25}) [k l c min_ref]) +(select {from: hits s0: (sum ResolutionWidth) s1: (sum (+ ResolutionWidth 1)) s2: (sum (+ ResolutionWidth 2)) s3: (sum (+ ResolutionWidth 3)) s4: (sum (+ ResolutionWidth 4)) s5: (sum (+ ResolutionWidth 5)) s6: (sum (+ ResolutionWidth 6)) s7: (sum (+ ResolutionWidth 7)) s8: (sum (+ ResolutionWidth 8)) s9: (sum (+ ResolutionWidth 9)) s10: (sum (+ ResolutionWidth 10)) s11: (sum (+ ResolutionWidth 11)) s12: (sum (+ ResolutionWidth 12)) s13: (sum (+ ResolutionWidth 13)) s14: (sum (+ ResolutionWidth 14)) s15: (sum (+ ResolutionWidth 15)) s16: (sum (+ ResolutionWidth 16)) s17: (sum (+ ResolutionWidth 17)) s18: (sum (+ ResolutionWidth 18)) s19: (sum (+ ResolutionWidth 19)) s20: (sum (+ ResolutionWidth 20)) s21: (sum (+ ResolutionWidth 21)) s22: (sum (+ ResolutionWidth 22)) s23: (sum (+ ResolutionWidth 23)) s24: (sum (+ ResolutionWidth 24)) s25: (sum (+ ResolutionWidth 25)) s26: (sum (+ ResolutionWidth 26)) s27: (sum (+ ResolutionWidth 27)) s28: (sum (+ ResolutionWidth 28)) s29: (sum (+ ResolutionWidth 29)) s30: (sum (+ ResolutionWidth 30)) s31: (sum (+ ResolutionWidth 31)) s32: (sum (+ ResolutionWidth 32)) s33: (sum (+ ResolutionWidth 33)) s34: (sum (+ ResolutionWidth 34)) s35: (sum (+ ResolutionWidth 35)) s36: (sum (+ ResolutionWidth 36)) s37: (sum (+ ResolutionWidth 37)) s38: (sum (+ ResolutionWidth 38)) s39: (sum (+ ResolutionWidth 39)) s40: (sum (+ ResolutionWidth 40)) s41: (sum (+ ResolutionWidth 41)) s42: (sum (+ ResolutionWidth 42)) s43: (sum (+ ResolutionWidth 43)) s44: (sum (+ ResolutionWidth 44)) s45: (sum (+ ResolutionWidth 45)) s46: (sum (+ ResolutionWidth 46)) s47: (sum (+ ResolutionWidth 47)) s48: (sum (+ ResolutionWidth 48)) s49: (sum (+ ResolutionWidth 49)) s50: (sum (+ ResolutionWidth 50)) s51: (sum (+ ResolutionWidth 51)) s52: (sum (+ ResolutionWidth 52)) s53: (sum (+ ResolutionWidth 53)) s54: (sum (+ ResolutionWidth 54)) s55: (sum (+ ResolutionWidth 55)) s56: (sum (+ ResolutionWidth 56)) s57: (sum (+ ResolutionWidth 57)) s58: (sum (+ ResolutionWidth 58)) s59: (sum (+ ResolutionWidth 59)) s60: (sum (+ ResolutionWidth 60)) s61: (sum (+ ResolutionWidth 61)) s62: (sum (+ ResolutionWidth 62)) s63: (sum (+ ResolutionWidth 63)) s64: (sum (+ ResolutionWidth 64)) s65: (sum (+ ResolutionWidth 65)) s66: (sum (+ ResolutionWidth 66)) s67: (sum (+ ResolutionWidth 67)) s68: (sum (+ ResolutionWidth 68)) s69: (sum (+ ResolutionWidth 69)) s70: (sum (+ ResolutionWidth 70)) s71: (sum (+ ResolutionWidth 71)) s72: (sum (+ ResolutionWidth 72)) s73: (sum (+ ResolutionWidth 73)) s74: (sum (+ ResolutionWidth 74)) s75: (sum (+ ResolutionWidth 75)) s76: (sum (+ ResolutionWidth 76)) s77: (sum (+ ResolutionWidth 77)) s78: (sum (+ ResolutionWidth 78)) s79: (sum (+ ResolutionWidth 79)) s80: (sum (+ ResolutionWidth 80)) s81: (sum (+ ResolutionWidth 81)) s82: (sum (+ ResolutionWidth 82)) s83: (sum (+ ResolutionWidth 83)) s84: (sum (+ ResolutionWidth 84)) s85: (sum (+ ResolutionWidth 85)) s86: (sum (+ ResolutionWidth 86)) s87: (sum (+ ResolutionWidth 87)) s88: (sum (+ ResolutionWidth 88)) s89: (sum (+ ResolutionWidth 89))}) +(select {from: hits by: [SearchEngineID ClientIP] c: (count ClientIP) sum_ref: (sum IsRefresh) avg_res: (avg ResolutionWidth) where: (!= SearchPhrase "") desc: c take: 10}) +(select {from: hits by: [WatchID ClientIP] c: (count ClientIP) sum_ref: (sum IsRefresh) avg_res: (avg ResolutionWidth) where: (!= SearchPhrase "") desc: c take: 10}) +(select {from: hits by: [WatchID ClientIP] c: (count ClientIP) sum_ref: (sum IsRefresh) avg_res: (avg ResolutionWidth) desc: c take: 10}) +(select {from: hits by: URL c: (count URL) desc: c take: 10}) +(select {from: (select {from: hits by: URL c: (count URL) desc: c take: 10}) one: 1 URL: URL c: c}) +(select {from: hits by: {ClientIP: ClientIP ip1: (- ClientIP 1) ip2: (- ClientIP 2) ip3: (- ClientIP 3)} c: (count ClientIP) desc: c take: 10}) +(select {from: hits by: URL views: (count URL) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== DontCountHits 0) (== IsRefresh 0) (!= URL "")) desc: views take: 10}) +(select {from: hits by: Title views: (count Title) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== DontCountHits 0) (== IsRefresh 0) (!= Title "")) desc: views take: 10}) +(select {from: hits by: URL views: (count URL) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== IsRefresh 0) (!= IsLink 0) (== IsDownload 0)) desc: views take: [1000 10]}) +(select {from: hits by: {TraficSourceID: TraficSourceID SearchEngineID: SearchEngineID AdvEngineID: AdvEngineID Src: (if (and (== SearchEngineID 0) (== AdvEngineID 0)) Referer "") Dst: URL} views: (count URL) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== IsRefresh 0)) desc: views take: [1000 10]}) +(select {from: hits by: [URLHash EventDate] views: (count URLHash) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== IsRefresh 0) (in TraficSourceID [-1 6]) (== RefererHash 3594120000172545465)) desc: views take: [100 10]}) +(select {from: hits by: [WindowClientWidth WindowClientHeight] views: (count WatchID) where: (and (== CounterID 62) (within EventDate [2013.07.01 2013.07.31]) (== IsRefresh 0) (== DontCountHits 0) (== URLHash 2868770270353813622)) desc: views take: [10000 10]}) +(select {from: hits by: {M: (xbar EventTime 60000000000)} views: (count WatchID) where: (and (== CounterID 62) (within EventDate [2013.07.14 2013.07.15]) (== IsRefresh 0) (== DontCountHits 0)) asc: M take: [1000 10]}) diff --git a/rayforce/query b/rayforce/query new file mode 100755 index 0000000000..34217395ee --- /dev/null +++ b/rayforce/query @@ -0,0 +1,27 @@ +#!/bin/bash +# Reads a Rayfall query from stdin and runs it on the Rayforce server. +# Stdout: query result. Stderr: query runtime in fractional seconds on the last +# line. Exit non-zero on error. +set -e + +export RAYFORCE_PORT="${RAYFORCE_PORT:-5000}" +export RAYFORCE_QUERY RAYFORCE_MS +RAYFORCE_QUERY=$(mktemp) +RAYFORCE_MS=$(mktemp) +trap 'rm -f "$RAYFORCE_QUERY" "$RAYFORCE_MS"' EXIT + +cat > "$RAYFORCE_QUERY" + +rayforce query.rfl + +# Guard against reporting a query that produced no timing as 0 seconds. +ms=$(cat "$RAYFORCE_MS") +if [ -z "$ms" ]; then + echo "rayforce: query returned no timing" >&2 + exit 1 +fi + +# `timeit` reports milliseconds and Rayforce may render them in exponent form +# (5.6e-06); awk normalises whatever it printed into plain fractional seconds, +# which is the only shape the driver's timing parser accepts. +awk -v ms="$ms" 'BEGIN { printf "%.6f\n", ms / 1000 }' >&2 diff --git a/rayforce/query.rfl b/rayforce/query.rfl new file mode 100644 index 0000000000..63477c3777 --- /dev/null +++ b/rayforce/query.rfl @@ -0,0 +1,13 @@ +; Per-query client. Sends the Rayfall expression in $RAYFORCE_QUERY to the +; server, which times it with `timeit` (nanosecond clock, result in ms) and +; keeps the result in `rf-result`. A second, untimed round trip pulls the result +; back so ./query can print it. The elapsed milliseconds are written to +; $RAYFORCE_MS; ./query converts them to the fractional seconds the benchmark +; driver expects. +(set h (.ipc.open (format "127.0.0.1:%" (.os.getenv "RAYFORCE_PORT")) 60000)) +(set ms (.ipc.send h + (format "(timeit (set rf-result %))" (read (.os.getenv "RAYFORCE_QUERY"))))) +(show (.ipc.send h "rf-result")) +(write (.os.getenv "RAYFORCE_MS") (format "%" ms)) +(.ipc.close h) +(exit 0) diff --git a/rayforce/server.rfl b/rayforce/server.rfl new file mode 100644 index 0000000000..41d8c4bc81 --- /dev/null +++ b/rayforce/server.rfl @@ -0,0 +1,18 @@ +; Init script for the Rayforce IPC server (see ./start). +; +; Opening the splayed table is eager: Rayforce validates every column file and +; loads the symbol dictionary up front, so this costs seconds-to-minutes on the +; 100M-row dataset. Doing it here, once per server start, keeps that cost out of +; the per-query numbers instead of paying it in every one of the 129 query +; invocations a benchmark run makes. +; +; ./start runs before ./load in the benchmark driver, so the table directory may +; not exist yet on the very first start; `try` keeps the server up in that case. +; The driver stops and starts the server again before each query, and that later +; start picks the table up. +; +; The listening socket only starts accepting after this script returns, which is +; exactly the readiness signal ./check needs. + +(set hits (try (.db.splayed.get "hits") (fn [e] (println "hits not loaded yet") 0))) +(if (== (type hits) 'TABLE) (println (format "loaded % rows" (count hits))) 0) diff --git a/rayforce/start b/rayforce/start new file mode 100755 index 0000000000..431d35f152 --- /dev/null +++ b/rayforce/start @@ -0,0 +1,17 @@ +#!/bin/bash +# Starts the Rayforce IPC server (see server.rfl for why the table is opened +# once here rather than in every query process). +# +# stdin/stdout are not a terminal, so rayforce skips the REPL and runs only the +# IPC poll loop. The listening socket accepts connections only after server.rfl +# returns, which makes ./check a real readiness probe. +set -e + +port="${RAYFORCE_PORT:-5000}" + +if pgrep -x rayforce >/dev/null 2>&1; then + exit 0 +fi + +nohup rayforce -p "$port" server.rfl >> server.log 2>&1 & +echo $! > server.pid diff --git a/rayforce/stop b/rayforce/stop new file mode 100755 index 0000000000..c2c36c3cf0 --- /dev/null +++ b/rayforce/stop @@ -0,0 +1,13 @@ +#!/bin/bash +# Stops the Rayforce IPC server. The engine keeps its columns mmap'd, so the +# process has to be gone before the driver's drop_caches can evict them. +# +# `pkill -x` matches the process name, not the command line: matching on +# "rayforce -p " with -f would also match any shell whose own command line +# happens to contain that string. +if [ -f server.pid ]; then + kill "$(cat server.pid)" 2>/dev/null || true + rm -f server.pid +fi +pkill -x rayforce 2>/dev/null || true +exit 0 diff --git a/rayforce/template.json b/rayforce/template.json new file mode 100644 index 0000000000..e83e291944 --- /dev/null +++ b/rayforce/template.json @@ -0,0 +1,11 @@ +{ + "system": "Rayforce", + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": [ + "C", + "column-oriented", + "embedded" + ] +} From 74e151f0b6ec73f5208a0cb8f39f8ba61234c046 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:42:08 +0000 Subject: [PATCH 2/2] Add benchmark results for rayforce (c6a.4xlarge) --- rayforce/results/20260815/c6a.4xlarge.json | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 rayforce/results/20260815/c6a.4xlarge.json diff --git a/rayforce/results/20260815/c6a.4xlarge.json b/rayforce/results/20260815/c6a.4xlarge.json new file mode 100644 index 0000000000..01f4a2eaed --- /dev/null +++ b/rayforce/results/20260815/c6a.4xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Rayforce", + "date": "2026-08-15", + "machine": "c6a.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C","column-oriented","embedded"], + "load_time": 3939, + "data_size": 47305927077, + "concurrent_qps": 0.023, + "concurrent_error_ratio": 1, + "result": [ + [0, 0, 0], + [0.544, 0.006, 0.005], + [0.931, 0.181, 0.181], + [2.254, 0.022, 0.021], + [2.523, 0.502, 0.506], + [1.785, 0.134, 0.132], + [0.742, 0.13, 0.129], + [0.412, 0.014, 0.015], + [10.037, 5.076, 15.16], + [6.173, 0.892, 0.893], + [4.999, 0.312, 0.312], + [null, null, null], + [2.706, 2.048, 2.043], + [5.545, 1.482, 1.473], + [2.13, 0.973, 0.974], + [4.925, 1.537, 1.556], + [4.308, 11.387, 92.714], + [9.744, 6.206, 6.392], + [8.668, 40.035, 184.311], + [0.842, 0.014, 0.014], + [9.486, 0.423, 0.424], + [5.627, 1.496, 1.503], + [8.756, 2.25, 2.254], + [9.752, 0.341, 0.338], + [3.572, 0.03, 0.029], + [2.553, 0.115, 0.115], + [3.572, 0.03, 0.03], + [11.209, 0.681, 0.682], + [null, null, null], + [0.854, 0.02, 0.02], + [4.806, 1.097, 1.107], + [7.199, 1.191, 1.187], + [77.528, null, null], + [1.668, 0.999, 0.998], + [1.665, 0.995, 0.994], + [3.133, 1.474, 1.478], + [0.509, 0.463, 0.458], + [0.504, 0.46, 0.457], + [0.068, 0.011, 0.011], + [0.167, 0.103, 0.104], + [2.909, 0.052, 0.052], + [2.13, 0.029, 0.029], + [1.044, 0.162, 0.156] +] + } + \ No newline at end of file