aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 02:04:40 +0100
committereunhhu <[email protected]>2026-09-28 02:04:40 +0100
commit922ead6b88a1a4ff2edf093dba57063b19a75890 (patch)
tree8d0d622dc81501dbab22843cc4e16f87ec4cd15d
parentba3aaed37c9177cf7ef5f370dd6b8a616b045af9 (diff)
downloadpure-922ead6b88a1a4ff2edf093dba57063b19a75890.tar.gz
pure-922ead6b88a1a4ff2edf093dba57063b19a75890.zip
benchmarks, docs: add bank queries benchmark and outside feedback
-rw-r--r--benchmarks/bank/queries.py187
-rw-r--r--docs/outside-feedback/feedback-001.md57
-rw-r--r--docs/outside-feedback/feedback-002.md25
3 files changed, 269 insertions, 0 deletions
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 프론트엔드 최적화 추진.