Local-first, zero-network. 모든 상태는 ~/.flowkey/ 아래에만 존재.
C가 엔진이다. 핫패스(이벤트 수집 · 저널 · 매칭)는 C로,
UI/플러그인은 IPC 너머의 별도 프로세스.
마이크로 최적화 가능한 자료구조부터. 캐시 라인 정렬, SPSC lock-free
링, arena/bump 할당, 인터닝, 메모리 매핑 저널, 배치 fsync.
장애 격리. observer / pattern / runner 는 동일 프로세스 내 다른
스레드 또는 별 프로세스. 한 모듈 크래시가 전체를 죽이지 않음.
결정적 재실행. 워크플로는 IR(중간 표현)로 직렬화되고,
같은 입력 → 같은 효과 → 같은 undo.
┌──────────────────────────────┐
│ flowkeyd │
│ ┌────────┐ ┌───────────┐ │
FS / App ─▶ │ │observer│──▶│ ring(s) │──┼──▶ journal (mmap, append-only)
events │ └────────┘ └─────┬─────┘ │
│ ▼ │
│ ┌────────────┐ │
│ │ pattern │ │
│ │ engine │ │
│ └─────┬──────┘ │
│ ▼ │
│ ┌────────────┐ │
│ │ workflow │ │
│ │ builder │ │
│ └─────┬──────┘ │
│ ▼ │
│ ┌────────────┐ │
IPC (UDS) ◀──┼──────────────│ runner / │ │
│ │ scheduler │ │
│ └────────────┘ │
└──────────────────────────────┘
▲
│ ~/.flowkey/flowkey.sock
│
┌─────────┴──────────┐
│ flowctl / UI │
└────────────────────┘
observer 스레드: OS별 백엔드(macOS FSEvents → 나중에 inotify/ETW).
배압이 발생하면 이벤트는 drop with counter , 절대 차단하지 않음.
dispatcher 메인 루프: SPSC 링에서 이벤트를 빼서 journal에 append하고,
pattern 엔진과 IPC 구독자에게 fan-out.
runner : 워크플로 실행. 모든 부수효과는 txn을 통해 기록.
include/flowkey/ # public C 헤더
core.h 공통 매크로, 결과 타입
arena.h bump allocator
ring.h SPSC lock-free ring
hash.h fast hash (wyhash-style)
intern.h path/string interning
time.h monotonic ns
log.h leveled async log
event.h 이벤트 타입/직렬화
journal.h append-only journal
observer.h 관찰자 추상 인터페이스
pattern.h (stub) 패턴 엔진
workflow.h (stub) 워크플로 IR
runner.h (stub) 실행기
ipc.h UDS 프로토콜
src/
core/ 순수 C, 의존성 없음
platform/macos/ CoreServices / AppKit 브리지
platform/linux/ (예정) inotify, x11/wayland
observer/ 백엔드 등록 + fanout
journal/ 저널 구현
daemon/ flowkeyd main
ipc/ 소켓 서버/클라이언트
cli/ flowctl
tests/ 유닛 테스트 (단일 바이너리)
third_party/ (없음 — 의도적으로 zero-dep)
4.1 Arena (bump allocator)
청크 단위로 mmap 또는 malloc. 기본 64 KiB, growth 2x.
정렬: 기본 16 B, 사용자가 지정 가능.
fk_arena_reset()는 모든 청크를 보존하고 포인터만 되돌림 →
이벤트 윈도 처리에서 GC 비용 0.
단일 생산자(observer) / 단일 소비자(dispatcher).
용량은 2의 거듭제곱, 마스크 비트 연산.
head/tail을 서로 다른 캐시 라인 (64 B)에 배치 → false sharing 회피.
atomic_store_explicit(memory_order_release) /
atomic_load_explicit(memory_order_acquire)만 사용. 락 없음.
가득 차면 drop_count만 증가 (관측은 절대 OS를 막지 않는다).
struct fk_event { // 32 bytes — half cache line
uint64_t ts_ns ; // monotonic timestamp
uint64_t seq ; // 전역 시퀀스
uint32_t path_id ; // interned (primary)
uint32_t aux_id ; // interned (secondary, e.g. rename target)
uint16_t kind ; // fk_event_kind
uint16_t flags ;
uint32_t pid ;
};
페이로드(파일 내용 등)는 절대 이 구조체에 넣지 않는다. 큰 데이터는
journal의 별도 blob 영역에 offset으로 기록.
경로 문자열은 path table 에 저장하고 32-bit id로 참조.
open-addressing 해시(load factor ≤ 0.5), wyhash.
핵심 이득: 이벤트 비교 = 정수 비교, 저널 크기 감소.
~/.flowkey/journal/NNNNNN.log 세그먼트, 64 MiB rotate.
헤더 8 B + payload + CRC32C 4 B. 토른 라이트 감지 가능.
writer는 pwrite + 주기적 fdatasync (기본 250 ms 또는 64 KiB).
reader는 mmap(PROT_READ), 순차 scan은 prefetch hint 사용.
path table은 별도 paths.tab (append-only) + 메모리 캐시.
kind
의미
path_id
aux_id
FS_CREATE
파일/디렉터리 생성
경로
—
FS_MODIFY
내용 수정
경로
—
FS_RENAME
이름 변경
from
to
FS_DELETE
삭제
경로
—
APP_LAUNCH
앱 실행
bundle id
—
APP_EXIT
앱 종료
bundle id
—
WIN_FOCUS
윈도 포커스
bundle id
title id
CLIPBOARD
클립보드 변경
mime id
size
HOTKEY
등록된 단축키
hotkey id
—
Unix domain socket: ~/.flowkey/flowkey.sock (perm 0600).
메시지 = u32 len | u16 type | u16 flags | bytes payload.
페이로드 인코딩: v0는 사람이 읽기 쉬운 줄 기반 텍스트 (OK seq=…),
v1부터 길이 접두 바이너리(MessagePack-like) 도입.
7. 안전 실행 (Runner) — 설계 노트
모든 부수효과 op는 4개 함수로 정의된다:
plan(ctx) → dry_run(ctx) → apply(ctx, txn) → revert(ctx, txn).
txn은 journal에 op 단위로 기록되고, undo는 역순으로 revert 호출.
파일 삭제는 항상 휴지통 경유. never unlink directly.
rename/move는 동일 볼륨이면 renamex_np 사용, 다른 볼륨이면
copy → fsync → unlink(src) 3단계 + 중단점.
항목
목표
유휴 시 CPU
< 0.2% (M1, 사용자 home 관찰)
유휴 시 RSS
< 25 MiB
이벤트 처리 지연
p99 < 1 ms (observer → journal append)
이벤트 손실
정상 부하 0, 폭주 시 drop count 보고
디스크 쓰기
≤ 200 KiB/min (평균 사용자)
데몬 시작
< 50 ms (cold)
윈도 좌표 / 픽셀 매크로
클라우드 동기화
멀티 유저
iOS / mobile
표준 C11 + POSIX + 플랫폼 프레임워크만.
외부 라이브러리 추가는 PR에서 정당화 필요(third_party/ 명시).
빌드는 clang + make만으로 가능.