From 922ead6b88a1a4ff2edf093dba57063b19a75890 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 02:04:40 +0100 Subject: benchmarks, docs: add bank queries benchmark and outside feedback --- benchmarks/bank/queries.py | 187 ++++++++++++++++++++++++++++++++++ docs/outside-feedback/feedback-001.md | 57 +++++++++++ docs/outside-feedback/feedback-002.md | 25 +++++ 3 files changed, 269 insertions(+) create mode 100644 benchmarks/bank/queries.py create mode 100644 docs/outside-feedback/feedback-001.md create mode 100644 docs/outside-feedback/feedback-002.md diff --git a/benchmarks/bank/queries.py b/benchmarks/bank/queries.py new file mode 100644 index 0000000..7e794f8 --- /dev/null +++ b/benchmarks/bank/queries.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Queries over a bank of 100,000 accounts: Pure's registered queries beside +the same SQL on SQLite (docs/research/091-queries.md). + +The bank (bank.aup) gets --branches branches, ten tellers each, --accounts +accounts spread over the branches and --transfers transfers, posted over HTTP; +SQLite (in RAM, the pgbench tables) gets the same rows. Then each query is +asked --times times on both, Pure's through GET /queries/NAME (registered +once, shown from the last save's snapshot, HTTP and JSON included), SQLite's +through its C library from Python, on pgbench's tables as they are and again +with the indexes a DBA would add for these queries (accounts by balance, and +by branch with the balance) after ANALYZE. Pure is given no index: it plans +from the query alone. The answers are checked to agree. Prints one JSON line +per query: the median and fastest time of each. +""" +import argparse +import http.client +import json +import random +import select +import shutil +import sqlite3 +import statistics +import subprocess +import tempfile +import threading +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + +QUERIES = { + # per branch: its total and how many accounts (a GROUP BY, as a fold per row) + 'per_branch': ('view per_branch = each b in branches {\n bid = b.bid\n' + ' total = sum(a.balance for a in accounts where a.bid == b.bid)\n' + ' accounts = count(a in accounts where a.bid == b.bid)\n}\n', + 'SELECT bid, sum(abalance), count(*) FROM pgbench_accounts GROUP BY bid ORDER BY bid'), + # accounts in credit (a filter over every row) + 'in_credit': ('view in_credit = count(a in accounts where a.balance > 0)\n', + 'SELECT count(*) FROM pgbench_accounts WHERE abalance > 0'), + # large transfers with their account's and teller's balance (a three-way join) + 'large': ('view large = each h in history where h.delta > 4900 {\n aid = h.aid\n delta = h.delta\n' + ' account = accounts[h.aid].balance\n teller = tellers[h.tid].balance\n}\n', + 'SELECT h.aid, h.delta, a.abalance, t.tbalance FROM pgbench_history h ' + 'JOIN pgbench_accounts a ON a.aid = h.aid JOIN pgbench_tellers t ON t.tid = h.tid ' + 'WHERE h.delta > 4900 ORDER BY h.rowid'), + # the richest accounts and how many are richer (a correlated self-join) + 'ranks': ('view ranks = each a in accounts where a.balance > 4900 {\n aid = a.aid\n' + ' richer = count(b in accounts where b.balance > a.balance)\n}\n', + 'SELECT a.aid, (SELECT count(*) FROM pgbench_accounts b WHERE b.abalance > a.abalance) ' + 'FROM pgbench_accounts a WHERE a.abalance > 4900 ORDER BY a.aid'), +} + + +def post_all(port, path, bodies, threads=8): + failed = [] + + def worker(part): + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=60) + for body in part: + connection.request('POST', path, body, {'Content-Type': 'application/json'}) + answer = connection.getresponse() + answer.read() + if answer.status != 200: + failed.append(answer.status) + connection.close() + + workers = [threading.Thread(target=worker, args=(bodies[i::threads],)) for i in range(threads)] + for w in workers: + w.start() + for w in workers: + w.join() + assert not failed, failed[:3] + + +def rows_of(name, data): + """Pure's answer as SQLite's tuples.""" + if name == 'in_credit': + return [(int(data),)] + items = json.loads(data)['items'] + return [tuple(int(v) for v in row.values()) for row in items] + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--aup', type=Path, default=ROOT.parent / 'aup' / 'build' / 'aup') + parser.add_argument('--serve', type=Path, default=ROOT / 'build' / 'pure-serve') + parser.add_argument('--accounts', type=int, default=100000) + parser.add_argument('--branches', type=int, default=10) + parser.add_argument('--transfers', type=int, default=20000) + parser.add_argument('--times', type=int, default=15) + args = parser.parse_args() + work = Path(tempfile.mkdtemp(prefix='pure-queries-', dir='/dev/shm')) + process = None + try: + module = work / 'bank.pmod' + subprocess.run([str(args.aup), 'build', str(HERE / 'bank.aup'), '-o', str(module)], check=True) + compiled = {} + for name, (view, _) in QUERIES.items(): + source = work / f'{name}.aup' + source.write_text(view) + subprocess.run([str(args.aup), 'query', str(HERE / 'bank.aup'), str(source), '-o', str(work / f'{name}.pmod')], + check=True) + compiled[name] = (work / f'{name}.pmod').read_bytes() + process = subprocess.Popen(['taskset', '-c', '0-2', str(args.serve), '--module', str(module), '--store', + str(work / 'store'), '--port', '0', '--timeout-ms', '600000'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + select.select([process.stdout], [], [], 10) + port = json.loads(process.stdout.readline())['port'] + rng = random.Random(9) + tellers = 10 * args.branches + transfers = [(rng.randint(1, args.accounts), rng.randint(1, tellers), rng.randint(1, args.branches), + rng.randint(-5000, 5000)) for _ in range(args.transfers)] + post_all(port, '/inputs/branch', [json.dumps({'bid': b}).encode() for b in range(1, args.branches + 1)], 1) + post_all(port, '/inputs/teller', [json.dumps({'tid': t, 'bid': (t - 1) // 10 + 1}).encode() + for t in range(1, tellers + 1)], 1) + post_all(port, '/inputs/account', [json.dumps({'aid': a, 'bid': (a - 1) % args.branches + 1}).encode() + for a in range(1, args.accounts + 1)]) + post_all(port, '/inputs/transfer', [json.dumps({'aid': a, 'tid': t, 'bid': b, 'delta': d}).encode() + for a, t, b, d in transfers], 1) # one client: history in this order + db = sqlite3.connect(str(work / 'bank.db')) + db.executescript('PRAGMA journal_mode=WAL;' + 'CREATE TABLE pgbench_accounts (aid INTEGER PRIMARY KEY, bid INTEGER, abalance INTEGER, filler CHAR(84));' + 'CREATE TABLE pgbench_tellers (tid INTEGER PRIMARY KEY, bid INTEGER, tbalance INTEGER, filler CHAR(84));' + 'CREATE TABLE pgbench_branches (bid INTEGER PRIMARY KEY, bbalance INTEGER, filler CHAR(88));' + 'CREATE TABLE pgbench_history (tid INTEGER, bid INTEGER, aid INTEGER, delta INTEGER, mtime TIMESTAMP,' + ' filler CHAR(22));') + db.executemany('INSERT INTO pgbench_branches VALUES (?, 0, \'\')', [(b,) for b in range(1, args.branches + 1)]) + db.executemany('INSERT INTO pgbench_tellers VALUES (?, ?, 0, \'\')', + [(t, (t - 1) // 10 + 1) for t in range(1, tellers + 1)]) + db.executemany('INSERT INTO pgbench_accounts VALUES (?, ?, 0, \'\')', + [(a, (a - 1) % args.branches + 1) for a in range(1, args.accounts + 1)]) + for a, t, b, d in transfers: + db.execute('UPDATE pgbench_accounts SET abalance = abalance + ? WHERE aid = ?', (d, a)) + db.execute('UPDATE pgbench_tellers SET tbalance = tbalance + ? WHERE tid = ?', (d, t)) + db.execute('UPDATE pgbench_branches SET bbalance = bbalance + ? WHERE bid = ?', (d, b)) + db.execute('INSERT INTO pgbench_history (tid, bid, aid, delta) VALUES (?, ?, ?, ?)', (t, b, a, d)) + db.commit() + client = http.client.HTTPConnection('127.0.0.1', port, timeout=120) + for name, bytes_ in compiled.items(): + client.request('PUT', f'/queries/{name}', bytes_) + answer = client.getresponse() + answer.read() + assert answer.status == 200, (name, answer.status) + def timed(ask): + times, answer = [], None + for _ in range(args.times): + began = time.perf_counter() + answer = ask() + times.append(time.perf_counter() - began) + return answer, times + + def pure(name): + client.request('GET', f'/queries/{name}') + answer = client.getresponse() + data = answer.read() + assert answer.status == 200, (name, answer.status) + return data + + results = {} + for name, (_, sql) in QUERIES.items(): + data, pure_times = timed(lambda: pure(name)) + rows, sqlite_times = timed(lambda: db.execute(sql).fetchall()) + assert rows_of(name, data) == [tuple(int(v) for v in row) for row in rows], name + results[name] = (len(rows), pure_times, sqlite_times) + db.executescript('CREATE INDEX accounts_balance ON pgbench_accounts (abalance);' + 'CREATE INDEX accounts_branch ON pgbench_accounts (bid, abalance); ANALYZE;') + for name, (_, sql) in QUERIES.items(): + rows, indexed_times = timed(lambda: db.execute(sql).fetchall()) + count, pure_times, sqlite_times = results[name] + assert len(rows) == count, name + print(json.dumps({'query': name, 'rows': count, 'accounts': args.accounts, + 'pure_median_ms': round(statistics.median(pure_times) * 1e3, 2), + 'pure_fastest_ms': round(min(pure_times) * 1e3, 2), + 'sqlite_median_ms': round(statistics.median(sqlite_times) * 1e3, 2), + 'sqlite_fastest_ms': round(min(sqlite_times) * 1e3, 2), + 'sqlite_indexed_median_ms': round(statistics.median(indexed_times) * 1e3, 2), + 'sqlite_indexed_fastest_ms': round(min(indexed_times) * 1e3, 2)}), flush=True) + finally: + if process: + process.terminate() + process.communicate(timeout=60) + shutil.rmtree(work) + + +if __name__ == '__main__': + main() diff --git a/docs/outside-feedback/feedback-001.md b/docs/outside-feedback/feedback-001.md new file mode 100644 index 0000000..c4f99f4 --- /dev/null +++ b/docs/outside-feedback/feedback-001.md @@ -0,0 +1,57 @@ +# Yeowoo (Pure / aup) 발전 로드맵 및 실행 계획 + +작성일: 2026-09-26 +작성자: 문선우 +대상: Pure / aup 코어 로드맵 및 아키텍처 연계 검토 + +--- + +## 1. 현재 달성 상태 요약 (`000` ~ `077`) + +- **코어 런타임 & 스토리지 엔진**: + - `PURETRE2`: 단 1번의 `fdatasync`로 충돌 안전성(Crash-safety)을 보장하는 단일 파일 불변 B/Radix 트리 엔진 완성. (커밋 지연 2.4ms, 저널링 오버헤드 제거) + - `pure-serve`: `epoll` + 코어별 `SO_REUSEPORT` 샤딩 기반 고성능 HTTP/1.1 호스트. +- **실증 벤치마크 (Raspberry Pi 5, `077`)**: + - **내구성 쓰기(Durable POST)**: 초당 13,692건 (axum 180건, drogon 172건 대비 **약 76배**). + - **JSON 읽기**: 144.8k req/s (C++ 끝판왕 Drogon 144.5k와 동급, axum 대비 52% 우위). + - **피크 메모리(RSS)**: **3.4 MiB** (Drogon 11.6 MiB, axum 6.4 MiB 대비 최소). + - **추상화 제거**: 복잡한 라우터·DB·직렬화 코드 없이 `aup` 명세 22줄만으로 서비스 구현 입증. + +--- + +## 2. 단기 핵심 실행 과제 (Short-term / In Progress) + +1. **Vectored Loan (무복사 익스텐트 대여) 완성** + - 대용량 파일 서빙 시 런타임 복사(`memcpy`) 없이, 스토리지 매핑 익스텐트를 `writev` 벡터 버퍼로 직통 연결. + - 비동기 티켓 수명 종료 시 런타임 자원 자동 회수 검증. +2. **저장과 적용의 겹치기 (I/O 파이프라이닝)** + - 소유자 스레드가 디스크 `fdatasync` 대기 중 멈추지 않도록, 디스크 쓰기를 전용 I/O 스레드/큐로 분리하고 다음 배치 연산을 선행 처리(Double Buffering). +3. **읽기 독립화 (MVCC / 버전 읽기)** + - 쓰기 동기화 중에도 연결 스레드가 이전 커밋된 저장소 매핑을 락 없이 직접 읽을 수 있도록 분리. +4. **JSON 본문 역직렬화 확장** + - 단순 단일 필드 외 복수 text 필드 및 중첩 JSON 요청을 제로카피 오프셋 스캔으로 수신. + +--- + +## 3. 중기 확장 과제 (레거시 서버·런타임 스택 전면 대체) + +1. **비동기 아웃바운드 HTTP/소켓 클라이언트** + - 수신(Inbound) 전용을 넘어, 외부 서드파티 API(웹훅, 클라우드, 결제) 호출을 0-copy 티켓 기반으로 수행하는 비동기 클라이언트 스택 설계. +2. **동적 스케줄링 & 멀티 샤딩 (`--threads auto`)** + - `sysconf(_SC_NPROCESSORS_ONLN)` 기반 기기 코어 수 자동 감지. + - 키 해시 기반 단일 소유자 파티셔닝(Actor-style Core Sharding)으로 쓰기 병목 선형 확장. +3. **단일 심볼(Symbol) 벡터 규격** + - 폰트 글리프와 UI 아이콘을 통합하는 초경량 바이너리 벡터 포맷 설계. + - SVG XML 파싱 오버헤드를 배제하고 GPU 커맨드 버퍼로 1:1 직결. + +--- + +## 4. 장기 비전 (Yeowoo 생태계 & 독자 런타임 OS) + +1. **Vega 종합 인터페이스 라이브러리**: + - 단순 화면 렌더러를 넘어 디스플레이, 터치·제스처 입력, 오디오, 렌더 서페이스를 총괄하는 통합 인터페이스 계층. + - 가상 DOM이나 거대 씬 그래프 없이, `aup` 뷰 상태 변경 바이트를 화면 및 입출력 버퍼로 직통 바인딩. +2. **E2EE 초저지연 메신저**: + - 메모리 포렌식 면역(Zeroize 슬롯), 하드웨어 암호화(ChaCha20-Poly1305) 직결 P2P 통신. +3. **Ray Kernel / ArtOS**: + - 불필요한 레거시 POSIX/C 런타임 누더기를 걷어내고, 하드웨어 실리콘과 Pure 런타임이 직결되는 궁극의 미니멀 OS. diff --git a/docs/outside-feedback/feedback-002.md b/docs/outside-feedback/feedback-002.md new file mode 100644 index 0000000..ccdbfa8 --- /dev/null +++ b/docs/outside-feedback/feedback-002.md @@ -0,0 +1,25 @@ +# 현재 상태 + +- commit eca213a까지 진행 + +# 필요한 방향 + +1. 이론적/알고리즘적 신규성 한계: +- COW(Copy-On-Write) 트리, 단일 파일 스토리지, 태그 기반 헤드 레코드 배치는 기존 분산/스토리지 기법(LMDB, Bitcask, Arkeion 등)의 엔지니어링 최적화에 가까움. +- 뛰어난 시스템 최적화지, 기존에 없던 새로운 데이터 자료구조나 알고리즘은 없음. +- 안정성을 추구하는 것은 맞지만, 기술적 신규성 및 독창성 요소 필요. (기회가 되면 단계적 확장 검토) + +2. 범용성의 부재: +- SQLite나 Postgres처럼 임의의 복잡한 쿼리, 다중 테이블 조인, 범용 트랜잭션을 다루는 게 아님. +- aup가 정의한 단일 소유자 반응형 상태 모델에 최적화된 특수 목적 런타임이기 때문에 특정 워크로드(키-값 기반 단일 상태 전이)에서만 빠른 것이라는 규격을 깨야함. + +3. 그래픽스 트레이드 오프: +- FreeType을 버리고 단일 심볼 벡터로 간 건 혁신적이지만, 역으로 복잡한 텍스트 셰이핑(HarfBuzz 같은 다국어 합자/커닝 처리)이나 접근성(Accessibility) API는 포기한거임. +- 상용 OS 레벨의 범용 GUI 툴킷으로 가기엔 아직 갈 길이 멀음. + +4. 시스템 확장 및 성능 목표: +- 실사용 환경 및 상용화 생태계(Ray Kernel, ArtOS, Bell Design System) 확장을 위해 범용성과 사용성의 균형 유지 필요. +- 로우레벨 시스템 최적화와 함께 상위 계층에서의 개발 편의성 및 DX 고려. +- QUIC/TLS의 무거운 상태 머신 오버헤드를 극복할 수 있는 초경량 단일 패킷/원시 소켓 기반의 독립 전송 프로토콜(Pure Link) 설계 및 검증 필요. +- 모바일 및 엣지 환경에 최적화된 경량 OS 및 고성능 렌더링 파이프라인 지속 검토. +- 셰이더 및 포스트 프로세싱 컴퓨팅, 네이티브 렌더링 등 UI/NUI 프론트엔드 최적화 추진. -- cgit v1.2.3