aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHECKPOINT.md6
-rw-r--r--docs/design/017-apps-on-one-host.md30
-rw-r--r--docs/research/115-apps-that-change.md115
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl11
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl11
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-load.txt12
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl2
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl4
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl3
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl31
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl16
-rw-r--r--research/experiments/030-app-lifecycle/isolation.py86
-rw-r--r--research/experiments/030-app-lifecycle/items-wide.aup29
-rw-r--r--research/experiments/030-app-lifecycle/memory.py79
-rw-r--r--research/experiments/030-app-lifecycle/pairs.py45
-rwxr-xr-xresearch/experiments/030-app-lifecycle/runs.sh35
-rwxr-xr-xresearch/experiments/030-app-lifecycle/window.py33
46 files changed, 747 insertions, 23 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 3c53700..d179039 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -18,7 +18,7 @@
| HTTP 서비스 | 보기가 GET, 입력이 POST다(JSON 본문과 그 안의 그룹 객체를 제자리에서 읽는다, 라우터 코드 없음). 앱의 `send`는 이벤트와 함께 저장되고, `--send 출력=URL`로 다른 앱이나 HTTP 끝점에 순서대로 묶어 전해지며, 받는 앱은 번호마다 한 번 적용한다(서버를 죽였다 살려도 정확히 한 번). `--seal 출력=공개키`면 받는 노드의 키로 봉인해(HPKE, 전달선마다 세션 하나, 보내는 노드의 키로 인증) 사이의 중계가 읽지도 바꾸지도 못하고, 받는 노드는 `--sender 이름=키`로 보낸 이를 가린다(봉인 전달 462k/s, 봉인 없이 468k/s). 답은 저장 뒤에 나간다. 상태를 가진 앱은 소유자 스레드 하나가 입력을 적용하고, 연결 스레드는 마지막 저장의 스냅숏으로 보기를 답한다(쓰기가 동기화하는 동안에도 읽기는 기다리지 않는다) | 008 HTTP 호스트, [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [082](docs/research/082-json-in-place.md), [083](docs/research/083-delivery-over-http.md), [086](docs/research/086-sealed-sends.md), [088](docs/research/088-sealed-sessions.md) |
| 성능(Pi 5) | 2026-09-27, 같은 기기에서 axum·drogon과 함께 쟀다. 문서 서버(한 스레드): 64 B GET 139k/s(CPU 1초당 140k; axum 127k·63k, drogon 147k·63k), 1 MiB GET 2,362/s(axum 1,304, drogon 754), 충돌 안전 PUT은 axum-atomic의 5.1–5.8배. 앱 서비스: 내구성 있는 쓰기 64연결 21.3k/s(axum·drogon 내구 약 200/s), 합계 GET 126k/s·두 스레드 153k(axum 136k, drogon 151k), 20행 페이지 두 스레드 148k(axum 93k, drogon 138k), 쓰기와 섞인 읽기 119k/s(axum·drogon 내구 1.1–1.4k/s). 유휴 RSS 2.3 MiB(axum 2.8, drogon 10.9), 최대 3.2–5.8 MiB(axum 6.6–9.7, drogon 11.9–78.8) | [075](docs/research/075-http-host-against-axum.md), [077](docs/research/077-apps-as-http-services.md), [080](docs/research/080-snapshot-reads.md), [081](docs/research/081-tail-records.md), [087](docs/research/087-small-reads-and-idle-memory.md) |
| 노드 사이 전달 | 연결 없는 링크가 기본이다(`--send 출력=pure://호스트:포트/입력`, 2026-09-29 채택). 두 노드의 키로 첫 패킷부터 봉인해 보내고 핸드셰이크가 없다. 어느 쪽을 kill -9 해도, 받는 노드의 저장소를 다른 주소로 옮겨도 다음 패킷으로 이어지며 잃거나 겹친 기록이 없다(기기 1만 개가 초당 2,000건을 보내는 중에 저장이 멈춘 100 ms 칸 0). 쉬는 링크는 받는 노드 메모리 132 B(quinn 연결의 1/289). 에폭마다 키를 바꿔 전방 비밀성을 얻는다. 전달당 CPU는 1 KiB 이상에서 quinn·ngtcp2보다 25 % 넘게 적고 61 B에서 비긴다. 손실·재정렬·병목 경로(M6)에서 재정렬 아래 quinn·ngtcp2보다 많이 나르고, 무작위 손실 아래 같으며, 깨끗한 병목에서 틀의 바이트로 3–5 % 적다. 명세가 있고, 명세만 보고 쓴 파이썬 받는 쪽이 노드와 맞붙는다. 받는 입력은 필드를 이름으로 받는다(순서가 달라도, 받는 쪽 필드가 더 있어도 되고, 보낸 필드를 버리지는 않는다). 그래서 두 노드가 함께 쓰는 보드를 한 노드씩 올려도 멈추지 않고, 기다리는 보내기도 새 필드로 옮겨진다([105](docs/research/105-growing-a-shared-board.md), [014](docs/design/014-fields-by-name.md)) | [설계 011](docs/design/011-pure-transport.md), [명세 012](docs/design/012-link-protocol.md), [093](docs/research/093-connectionless-link.md)–[102](docs/research/102-adoption-and-spec.md) |
-| 여러 앱 | 한 호스트가 앱 여럿을 품는다(`pure-serve --apps`, 경로는 `/앱/…`). 앱마다 스레드를 두지 않고 정해진 실행 스레드가 차례로 돌리며, 앱마다 연결이 모이는 집 연결 스레드가 있다. 앱마다 실행량(쓴 그대로의 명령 수, 넘으면 422 `work`)·대기열(503 `busy`)·메모리 예산이 있고, 모듈이나 저장소를 열지 못한 앱은 그 앱만 멈춘다. 앱 하나에 120 KiB(프로세스 하나의 PSS의 9%), 앱 100개에도 스레드 셋. 한 앱이 몰릴 때 다른 앱의 p99는 앱마다 프로세스일 때의 0.5배, 둘 다 바쁠 때 처리량과 p99는 같다. 앱마다 보냄을 배선하고(`send=`), 링크 포트 하나가 모든 앱의 입력을 `앱/입력`으로 받는다. 같은 호스트의 앱끼리는 소켓 없이 건네 링크의 두 배(초당 114만 건)를 CPU 40%로 나른다. 링크와 전달선은 프로세스의 보내는 루프 하나가 보낸다(링크와 전달선 스무 개에 스레드 24개가 5개, 링크 +63%, 전달선 같음) | [017](docs/design/017-apps-on-one-host.md), [111](docs/research/111-apps-on-one-host.md), [112](docs/research/112-sending-between-apps.md), [113](docs/research/113-one-sending-loop.md) |
+| 여러 앱 | 한 호스트가 앱 여럿을 품는다(`pure-serve --apps`, 경로는 `/앱/…`). 앱마다 스레드를 두지 않고 정해진 실행 스레드가 차례로 돌리며, 앱마다 연결이 모이는 집 연결 스레드가 있다. 앱마다 실행량(쓴 그대로의 명령 수, 넘으면 422 `work`)·대기열(503 `busy`)·메모리 예산이 있고, 모듈이나 저장소를 열지 못한 앱은 그 앱만 멈춘다. 앱 하나에 120 KiB(프로세스 하나의 PSS의 9%), 앱 100개에도 스레드 넷(연결 하나, 실행 둘, 구성 파일을 읽는 하나). 한 앱이 몰릴 때 다른 앱의 p99는 앱마다 프로세스일 때의 0.5배, 둘 다 바쁠 때 처리량과 p99는 같다. 앱마다 보냄을 배선하고(`send=`), 링크 포트 하나가 모든 앱의 입력을 `앱/입력`으로 받는다. 같은 호스트의 앱끼리는 소켓 없이 건네 링크의 두 배(초당 114만 건)를 CPU 40%로 나른다. 링크와 전달선은 프로세스의 보내는 루프 하나가 보낸다(링크와 전달선 스무 개에 스레드 24개가 5개, 링크 +63%, 전달선 같음). 달리는 호스트가 SIGHUP에 구성 파일을 다시 읽어 앱을 더하고, 멈추고, 그 자리에서 바꾼다(상태는 메모리에서 이름으로 옮긴다: 100만 항목의 앱이 1.1 ms만 쉬고, 다른 앱의 p99는 그대로) | [017](docs/design/017-apps-on-one-host.md), [111](docs/research/111-apps-on-one-host.md), [112](docs/research/112-sending-between-apps.md), [113](docs/research/113-one-sending-loop.md), [115](docs/research/115-apps-that-change.md) |
| 메모리 한도 | 앱마다 상태의 예산이 있다(`--memory`). 목록의 항목, 텍스트, outbox, 들은 번호를 저장 형식의 바이트로 센다(어느 기기에서나 같다). 예산을 넘기며 늘리는 사건은 거절하고(503 `memory`) 줄이는 사건은 받는다. 받는 쪽이 늦으면 outbox가 차서 클라이언트까지 역압이 가고, 가득 찬 받는 쪽은 링크로 알려 보내는 쪽이 다시 보내지 않는다. 답(`--reply`)은 자기 보내기의 자리를 쓴다. 상태 4 MB를 준 노드는 부하 아래 상주 9.6–9.8 MiB에 멈춘다(예산 없이 30초에 45.9 MiB). 오래 붙든 스냅숏은 그때 상태 한 벌만 쥔다(전: 30초에 678 MiB) | [설계 013](docs/design/013-memory-budget.md), [104](docs/research/104-where-memory-goes.md) |
| 표면과 메신저 | 사람을 위한 표면이 노드 안에서 돈다(`pure-serve --surface`): 마지막 저장의 스냅숏을 제자리에서 읽고, 친 키와 글자는 HTTP 요청과 같은 줄로 소유자에게 간다. Vega의 디스플레이·픽셀이 그 표면이다. 두 노드의 메신저(Vega `examples/messenger.aup`)는 서로에게 봉인·인증해 보내고, 한 줄이 상대 노드에 보이기까지 RAM 저장소에서 p50 0.79 ms·p99 1.1 ms, SD 카드에서 p50 3.5 ms(꼬리는 SD의 동기화) | [설계 010](docs/design/010-surfaces.md), [089](docs/research/089-messenger.md) |
| 표 크기의 상태 | 목록이 영속 B+트리다(변경·키 탐색·저장본 위치 O(log n), 스냅숏 O(1), 1,600만 항목, 런타임 상태 1 GiB). pgbench의 TPC-B 같은 이체(세 표의 행과 이력, 한 트랜잭션)가 10만 계좌에서 RAM 24.5k/s(전: 3만 계좌 3.2k/s), SD 카드 내구 1,339/s(SQLite WAL FULL 84–153/s) | [090](docs/research/090-tables.md) |
@@ -32,8 +32,8 @@
+9%, 상주 −10%, [107](docs/research/107-snapshot-nodes-reused.md)). 남은 가장 큰 몫은 항목 찾기다. 키로 나눈 소유자는 그다음에 본다. 대신 압축한 파일이 한 주기의 0을 미리 가져 커밋이 새
블록을 받지 않는다(저장소 커밋 +21%, 저널 커밋 −59%, [085](docs/research/085-cycle-sized-files.md)).
-남은 것: macOS의 호스트(`pure-serve`는 리눅스의 것), 달리는 호스트의 앱을 더하고 바꾸기(017의 3단계), 압축을 없애는 제자리 재사용 저장소(085 다음), 응답을 기다리는 출력의 묶음 전달(083), 앱
-라우트의 인증·TLS(문서 피어의 TLS는 있다), aup·Vega의 배포 묶음, 캐시보다 큰 목록을 찾는 걸음(109: 미리 가져오기로는 줄지
+남은 것: macOS의 호스트(`pure-serve`는 리눅스의 것), 바뀐 모듈을 놓기(바꿀 때마다 19 KB, 115), 압축을 없애는 제자리 재사용 저장소(085 다음), 응답을 기다리는 출력의 묶음 전달(083), 앱
+라우트의 인증·TLS(017의 3b, 문서 피어의 TLS는 있다), aup·Vega의 배포 묶음, 캐시보다 큰 목록을 찾는 걸음(109: 미리 가져오기로는 줄지
않았다. 한 사건의 조회를 겹치거나 구조를 바꿔야 한다).
## 공개할 작업물과 완료 증거
diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md
index f9ff765..3f2ce2b 100644
--- a/docs/design/017-apps-on-one-host.md
+++ b/docs/design/017-apps-on-one-host.md
@@ -1,7 +1,8 @@
# 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계
작성일: 2026-10-01. 상태: 1·2단계와 2b 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md),
-[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계는 착수 기준을 적었다(2026-10-04).
+[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계(수명) 구현, 검사로 고정하고
+쟀다([115](../research/115-apps-that-change.md)). 3b(경로의 인증)는 설계 전.
[외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델(
[007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다.
@@ -238,17 +239,20 @@ counter apps/count.pmod stores/counter
- 새 줄의 앱은 시작한다. 1단계의 시작과 같다(모듈 검증, 저장소, 배선, 실패하면 그 앱만 멈추고 까닭을 남긴다).
- 없어진 줄의 앱은 멈춘다(아래의 순서). 그 뒤로 그 경로는 404 `no_app`이다.
-- 바뀐 줄의 앱은 멈추고 새 줄로 다시 시작한다. 모듈 파일의 내용, 저장소, 예산, 배선 가운데 하나라도 바뀐 줄이다. 상태는
- 저장소에서 이름으로 이어받는다(지금 다른 모듈로 다시 띄울 때와 같다, `server_upgrade`).
+- 바뀐 줄의 앱은 그 자리에서 바뀐다. 모듈 파일의 내용, 예산, 배선 가운데 하나라도 바뀐 줄이다. 받은 일을 다 적용하고 저장한
+ 뒤, 메모리의 상태를 그대로 두고 모듈을 바꾼다(`pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 저장소는 열린
+ 채이고, 출구만 새 모듈로 다시 만든다. 새 모듈에 상태가 들어가지 않으면 그 앱은 멈추고 까닭을 남긴다.
+- 저장소가 바뀐 줄의 앱, 저장이 불확실해진 앱은 멈추고 저장소에서 다시 연다(상태는 이름으로 이어받는다, `server_upgrade`).
- 그대로인 줄의 앱은 건드리지 않는다. 멈춰 있거나(시작하지 못했거나) 불확실해진 앱만 다시 시작한다. 저장소에서 다시 연다.
-- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다. 앱마다 상태와 까닭, 그리고 이번에 한 일(시작, 멈춤, 다시 시작)이다.
+- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다(`{"reading":N,"apps":[…]}`). 앱마다 상태와 까닭, 그리고 이번에 한
+ 일이다(kept, changed, restarted, started, removed).
- 파일을 읽지 못하거나 틀린 줄이 있으면 아무것도 바꾸지 않고 까닭을 낸다.
HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은 경로의 인증(3b)을 먼저 요구한다. 신호는 프로세스의 주인만 보낼 수 있고,
파일은 호스트의 구성을 그대로 남긴다(다시 띄워도 같은 모습이다). 다시 읽기는 호스트의 스레드 하나(`pure-apps`)가 한다. 연결
스레드와 실행 스레드는 기다리지 않는다.
-**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다.
+**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다(그 자리에서 바꿀 때는 1–3까지 하고, 모듈을 바꾼 뒤 이어 간다).
1. 새 일을 받지 않는다. 그 앱의 경로는 503 `stopped`다. 링크와 안쪽 출구의 기록은 받지 않는다(보내는 쪽이 쥐고 다시 보낸다).
2. 소유자가 이미 받은 일을 다 적용하고, 저장하고, 답한다.
@@ -279,11 +283,25 @@ HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은
**하지 않는 것.**
-- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 멈추고 다시 여는 동안 그 앱은 503을 답한다. 그 시간을 잰다.
+- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 바꾸는 동안 그 앱은 503을 답한다. 그 자리에서 바꾸면 1 ms 남짓이다(기록의
+ 모양이 바뀌는 큰 목록은 목록을 다시 쓰는 동안).
- HTTP로 앱을 더하고 바꾸는 길. 경로의 인증(3b) 뒤에 따로 정한다.
**3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지다(CHECKPOINT의 남은 것). 3단계의 수명과 따로 설계한다.
+### 3단계에서 알게 된 것
+
+- **멈추고 다시 열면 다시 읽기가 전부였다.** 처음 구현(`12878cf`)은 바뀐 앱을 멈추고 저장소에서 다시 열었다. 항목 100만 개의
+ 앱이 150.6 ms를 답하지 못해 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 그 가운데 142 ms가 저장소를 다시 읽는 일이었다.
+ 저장소가 그대로인 앱을 그 자리에서 바꾸자(`7daf406`) 1.12 ms가 됐다(1,024개 1.06 ms). 기록의 모양이 바뀌는 모듈은 목록을 새
+ 모양으로 다시 쓰느라 171.5 ms다(다시 띄우면 332 ms).
+- **결과.** 한 앱을 거듭 바꾸는 동안 다른 앱의 p99는 0.97배, 앱 하나의 호스트는 TPC-C +0.7%와 명령 수 +0.02% 이하, 모듈을 바꿀
+ 때마다 19 KB가 남는다. 파일을 읽는 스레드가 하나 늘었다(앱 100개에 스레드 넷). 커밋: Pure `14c75d0`(해석한 모듈과 앱의 자리),
+ `1d892e2`(링크 스트림의 입력을 다시 찾기), `12878cf`(SIGHUP과 멈추고 다시 열기), `7daf406`(그 자리에서 바꾸기), `f5e9db5`와
+ 그 뒤(측정).
+- **검사가 찾은 것.** 보내는 앱이 멈추는 동안 받는 앱의 차례가 놓인 출구를 읽던 자리, 시작하기 전의 앱의 락을 다른 앱의 출구가
+ 쓰던 것. 둘 다 바꾸기 한가운데의 보내는 앱과 받는 앱을 함께 바꿔 보는 검사로 드러났다.
+
## 3단계의 착수 기준
- **바꿀 결정:** 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지다. 아래를 채우지 못하면 앱을 바꿀 때 호스트를
diff --git a/docs/research/115-apps-that-change.md b/docs/research/115-apps-that-change.md
new file mode 100644
index 0000000..a76f9dd
--- /dev/null
+++ b/docs/research/115-apps-that-change.md
@@ -0,0 +1,115 @@
+# 115 — 달리는 호스트의 앱을 더하고, 멈추고, 바꾸기
+
+2026-10-04. [017](../design/017-apps-on-one-host.md)의 3단계를 잰다. 기준은 구현하기 전에 017에 적어 커밋했다(`e4c7f7e`).
+
+- 바꿀 결정: 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지. 채우지 못하면 앱을 바꿀 때 호스트를 다시 띄운다.
+- 통과 조건(RAM의 Pi 5):
+ - 바꾸는 앱이 답하지 못하는 시간이 호스트를 다시 띄울 때보다 짧다(항목 1,000개와 100만 개).
+ - 한 앱을 거듭 바꾸는 동안 다른 앱(한 연결)의 p99가 바꾸지 않을 때의 1.5배를 넘지 않는다.
+ - 앱 하나의 호스트는 그대로다(TPC-C와 은행 질의의 중앙값이 3% 넘게 느려지지 않고, 명령 수가 1% 넘게 늘지 않는다).
+ - 바꿀 때마다 남는 메모리를 밝힌다.
+
+## 쉬운 말로
+
+- **통과했다.** 호스트가 SIGHUP에 `--apps` 파일을 다시 읽고, 줄이 없어진 앱은 멈추고, 새 줄의 앱은 시작하고, 바뀐 줄의 앱은 그
+ 자리에서 바꾼다. 항목 100만 개의 앱이 모듈을 바꾸는 동안 답하지 못한 시간은 1.1 ms다(호스트를 다시 띄우면 150 ms). 그동안
+ 다른 앱의 p99는 그대로다(0.97배). 앱 하나의 호스트도 그대로다.
+- **처음 구현은 큰 앱에서 기준을 넘지 못했다.** 앱을 멈추고 저장소에서 다시 여는 것으로 바꿨다. 100만 항목의 앱은 150.6 ms를
+ 답하지 못했다. 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 시간을 나눠 보니 거의 전부(142 ms)가 저장소를 다시 읽는
+ 데(`pure_app_start`) 들었다. 호스트를 다시 띄울 때도 같은 일을 하니 짧아질 수 없었다.
+- **고친 것은 다시 읽지 않는 것이다.** 저장소가 그대로인 앱은 받은 일을 다 적용하고 저장한 뒤, 메모리의 상태를 그대로 두고
+ 모듈만 바꾼다(코어의 `pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 출구만 새 모듈로 다시 만든다. 그러자 크기와
+ 상관없이 1 ms 남짓이 됐다. 기록의 모양이 바뀌는 모듈(필드가 하나 늘어난 기록)은 목록을 새 모양으로 다시 만들고 이미지를 다시
+ 써야 해서 172 ms가 들었다. 호스트를 다시 띄우면 다시 읽기에 그 일까지 더해 332 ms다.
+- **모듈을 바꿀 때마다 19 KB가 남는다.** 바뀐 모듈을 놓지 않는다. 그 모듈로 해석된 요청과 스냅숏이 아직 그것을 가리킬 수 있기
+ 때문이다. 예산이나 배선만 바뀌면 남는 것이 없다.
+
+## 결과
+
+원시 결과와 명령은 [030-app-lifecycle](../../research/experiments/030-app-lifecycle/)에 있다(`runs.sh`의 단계 `build`, `window`,
+`isolation`, `pairs`, `counts`, `memory`; `window.py`, `isolation.py`, `memory.py`, `pairs.py`; 앱 `items.aup`과 그 다음 판
+`items-next.aup`(보기 하나 더), `items-wide.aup`(기록에 필드 하나 더)).
+
+**답하지 못하는 시간**(`window.py`). 호스트(`--apps`, 저장소는 RAM, 코어 0–2)에 앱 하나가 항목을 든다(사건 하나가 목록을 두 배로
+늘려 1,024개나 1,048,576개). 클라이언트(코어 3)가 한 연결로 쓰기를 쉬지 않고 보내고, 답을 받은 두 쓰기 사이의 가장 긴 틈을 잰다.
+바꾸기는 줄이 다른 모듈 파일을 가리키게 하고 SIGHUP을 보내는 것이다(두 모듈을 번갈아). 다시 띄우기는 같은 변경을 전처럼 하는
+것이다(줄을 바꾸고 SIGTERM, 다시 시작). 1,024개는 바꾸기 20번과 다시 띄우기 10번, 1,048,576개는 10번과 5번의 중앙값이다.
+
+| 앱, 바뀌는 것 | 멈추고 다시 열기(`12878cf`) | 그 자리에서(`7daf406`) | 호스트를 다시 띄우기 |
+|---|---:|---:|---:|
+| 1,024개, 보기 하나 더 | 1.52 ms | **1.06 ms** | 2.42 ms |
+| 1,024개, 기록에 필드 하나 더 | 1.58 ms | **1.31 ms** | 2.51–2.64 ms |
+| 1,048,576개, 보기 하나 더 | 150.42 ms | **1.12 ms** | 149.63–149.94 ms |
+| 1,048,576개, 기록에 필드 하나 더 | 337.33 ms | **171.54 ms** | 329.85–332.42 ms |
+
+다시 띄우기의 범위는 두 빌드에서 잰 값이다. 처음 잰 것(`f5e9db5`)은 다시 띄울 때 모듈을 바꾸지 않았다(1,024개 1.59 대 3.05 ms,
+100만 개 150.64 대 149.73 ms). 비교가 같은 변경이 되도록 고쳐 다시 쟀다.
+
+멈추고 다시 열 때 100만 항목의 시간을 나누면, 멈추기 7–8 ms(상태를 놓기), 시작 142 ms(저장소를 다시 읽기)다. 호스트를 다시
+띄울 때의 시작은 170 ms였다. 그 자리에서 바꾸면 이 둘이 없다. 기록의 모양이 바뀌면 목록 100만 항목을 새 모양으로 다시 만들고
+이미지를 쓰는 데 약 170 ms가 든다. 다시 띄울 때도 같은 일을 다시 읽기에 더해서 한다.
+
+**다른 앱의 꼬리**(`isolation.py`). 같은 호스트(연결 스레드 둘, 실행 스레드 둘)에 100만 항목의 앱 A와 점수판 B. B에 한 연결의
+점수를 10초 보내는 동안 A를 그대로 두거나 바꾼다. 바꾸는 쪽은 보기 하나 더인 모듈로 0.2초마다(판마다 50번), 기록에 필드 하나
+더인 모듈로 0.4초마다(판마다 18번, 한 번에 약 170 ms의 일)다. 다섯 라운드, 순서를 번갈아.
+
+| B의 p99 중앙값 | 그대로 | 바꾸는 동안 | 비 |
+|---|---:|---:|---:|
+| 보기 하나 더, 0.2초마다 | 120 µs | 116 µs | 0.97 |
+| 기록에 필드 하나 더, 0.4초마다 | 120 µs | 116 µs | 0.97 |
+
+B는 5xx를 하나도 받지 않았다. 바꾸는 일은 파일을 읽는 스레드(`pure-apps`)에서 하고, 실행 스레드는 그 앱의 차례만 쉰다.
+
+**앱 하나의 호스트**(`runs.sh pairs`, 3단계 전 `e4c7f7e`와 후 `7daf406`, TPC-C W=1 64터미널 20초와 은행 질의, 여섯 쌍 번갈아,
+부하 평균 0.9–1.5).
+
+| | 전 | 후 |
+|---|---:|---:|
+| TPC-C 초당 거래 중앙값 | 42,900 | 43,220 (+0.7%) |
+| 거래당 CPU | 44.4 µs | 44.4 µs |
+| `per_branch`, `in_credit`, `large`, `ranks` | 5.31, 0.85, 0.38, 6.81 ms | +0.1%, −2.4%, 0.0%, −0.7% |
+
+쌍마다 TPC-C는 −3.8%에서 +6.2%로 흔들렸다(소유자의 코어 몫 0.87–0.91). 명령 수(025의 `count.py`, 전과 후를 두 번씩):
+
+| 한 번 물을 때 | 전 | 후 |
+|---|---:|---:|
+| `per_branch` | 26.980M | 26.827M (−0.57%) |
+| `in_credit` | 3.471M | 3.437M (−0.99%) |
+| `large` | 1.445M | 1.445M (+0.01%) |
+| `ranks` | 24.170M | 24.087M (−0.35%) |
+| `none`, `field`, `found` | 0.607M, 8.776M, 15.691M | +0.00%, +0.00%, +0.02% |
+
+요청이 해석한 모듈을 들고 다니고 소유자와 보기가 그것을 견주는 일(원자적 읽기와 포인터 비교)은 보이지 않는다.
+
+**남는 메모리**(`memory.py`). 항목 1,000개의 앱을 100번 바꿔 데운 뒤, 1,000번 더 바꾼 앞뒤의 자기만의 더티 메모리.
+
+| 바꾸는 것 | 앞 → 뒤 | 한 번에 |
+|---|---:|---:|
+| 모듈(두 파일을 번갈아, 바꿀 때마다 하나를 읽는다) | 3,120 → 21,952 KiB | 19,284 B |
+| 큐 예산만(모듈은 그대로) | 1,200 → 1,200 KiB | 0 B |
+
+한 번에 남는 것은 바뀐 모듈 하나(구조체 18,760 B와 모듈의 바이트)다.
+
+**검사**(`tests/check.py`의 `server_apps_lifecycle`). 한 호스트에서 앱을 더하고(바로 답한다), 지우고(404 `no_app`, 다른 프로세스가 그
+저장소를 연다), 바꾼다. 받는 앱 둘(같은 호스트의 앱이 안쪽 출구로, 다른 노드가 링크로 보내는)과 보내는 앱 둘을 보내기 한가운데에서
+바꾼다(바꿀 때 보낸 수가 0과 600 사이임을 단언한다). 받는 앱의 새 모듈은 입력의 번호가 다르고 보냄을 이름으로 받는다. 그동안
+목록 보기를 쉬지 않고 읽는다. 600건씩이 모두 한 번씩 순서대로 도착하고, 200을 받은 쓰기는 한 번씩 적용되고 503 `stopped`를 받은
+쓰기는 적용되지 않으며, 보기는 200이나 503 `stopped`만 답하고, 그대로인 앱은 하나도 실패하지 않는다. 저장소를 옮긴 앱은 새 저장소로
+다시 시작하고, 저장소를 만들 수 없던 앱은 고친 뒤의 SIGHUP에 시작하고, 틀린 줄이 있는 파일은 아무것도 바꾸지 않고, `kill -9` 뒤에는
+파일 그대로 시작한다. 혼자 10번, CPU 부하 아래 5번, ASan으로 빌드한 `pure-serve`로 6번 지났다.
+
+이 검사가 처음 구현에서 찾은 것: 보내는 앱이 멈추는 동안 받는 앱의 차례가 그 출구를 놓은 뒤에 읽는 자리(`local_done`), 앱이
+시작하기 전에 다른 앱의 출구가 그 소유자의 락을 쓰는 것(락을 자리와 함께 만든다).
+
+## 판정
+
+- **017의 3단계(수명)는 들어간다.** 네 조건을 모두 채웠다. 달리는 호스트의 앱은 SIGHUP과 파일로 더하고, 멈추고, 바꾼다.
+- 바꾸기는 그 자리에서 한다. 저장소가 바뀐 앱, 저장이 불확실해진 앱, 달리지 않던 앱만 저장소에서 다시 연다.
+- 파일을 읽는 스레드(`pure-apps`)가 여러 앱의 호스트마다 하나 늘었다. 앱 100개의 호스트는 스레드 넷이다(111에서 셋).
+
+## 다음
+
+- **바뀐 모듈을 놓기.** 지금은 바꿀 때마다 19 KB가 남는다(하루 한 번 바꾸면 한 해 7 MB). 놓으려면 그 모듈로 해석된 요청이
+ 끝났는지(본문을 받는 중인 요청은 `convert`에서 모듈을 다시 읽는다)와 연결 스레드의 리더가 다른 모듈로 옮겼는지를 알아야 한다.
+- **3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지. 그 뒤에 HTTP로 앱을 더하고 바꾸는 길.
+- 기록의 모양이 바뀌는 큰 목록의 교체(100만 항목 172 ms)는 목록을 새 모양으로 다시 쓰는 값이다. 그동안 그 앱만 쉰다.
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl
new file mode 100644
index 0000000..ef2f8da
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-counts.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 26963671, "cycles": 12117047}, "in_credit": {"instructions": 3467201, "cycles": 1640735}, "large": {"instructions": 1444754, "cycles": 661005}, "ranks": {"instructions": 24161321, "cycles": 15027140}, "none": {"instructions": 606875, "cycles": 190360}, "field": {"instructions": 8775999, "cycles": 3192408}, "found": {"instructions": 15689975, "cycles": 12571107}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26806316, "cycles": 13002802}, "in_credit": {"instructions": 3432116, "cycles": 1631717}, "large": {"instructions": 1444859, "cycles": 640909}, "ranks": {"instructions": 24076292, "cycles": 15205969}, "none": {"instructions": 606877, "cycles": 188197}, "field": {"instructions": 8776043, "cycles": 3139598}, "found": {"instructions": 15694123, "cycles": 12591284}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26996185, "cycles": 12941272}, "in_credit": {"instructions": 3474535, "cycles": 1773102}, "large": {"instructions": 1444802, "cycles": 647633}, "ranks": {"instructions": 24178845, "cycles": 15143526}, "none": {"instructions": 606875, "cycles": 189406}, "field": {"instructions": 8775999, "cycles": 3153492}, "found": {"instructions": 15692319, "cycles": 12541556}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26847256, "cycles": 11982057}, "in_credit": {"instructions": 3441216, "cycles": 1619426}, "large": {"instructions": 1444859, "cycles": 647753}, "ranks": {"instructions": 24096966, "cycles": 15147066}, "none": {"instructions": 606877, "cycles": 186693}, "field": {"instructions": 8776043, "cycles": 3133151}, "found": {"instructions": 15693340, "cycles": 12583251}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl
new file mode 100644
index 0000000..7cad56d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-next.jsonl
@@ -0,0 +1,11 @@
+{"round": 1, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10673, "completed": 106733, "2xx": 106733, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 120, "max_us": 5888}
+{"round": 1, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 11075, "completed": 110753, "2xx": 110753, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 5376}
+{"round": 2, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 11225, "completed": 112254, "2xx": 112254, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 4608}
+{"round": 2, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10195, "completed": 101955, "2xx": 101955, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10829, "completed": 108295, "2xx": 108295, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10323, "completed": 103233, "2xx": 103233, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 128, "max_us": 6400}
+{"round": 4, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10461, "completed": 104614, "2xx": 104614, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4352}
+{"round": 4, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10829, "completed": 108294, "2xx": 108294, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 120, "max_us": 4864}
+{"round": 5, "way": "quiet", "items": 1048576, "next": "items-next", "changes": 0, "requests_per_second": 10061, "completed": 100607, "2xx": 100607, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 128, "max_us": 5632}
+{"round": 5, "way": "changing", "items": 1048576, "next": "items-next", "changes": 50, "requests_per_second": 10761, "completed": 107611, "2xx": 107611, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 116, "max_us": 3968}
+{"items": 1048576, "next": "items-next", "every_s": 0.2, "quiet_p99_us": 120, "changing_p99_us": 116, "ratio": 0.97}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl
new file mode 100644
index 0000000..f9783a7
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-isolation-in-place-1000000-items-wide.jsonl
@@ -0,0 +1,11 @@
+{"round": 1, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10691, "completed": 106907, "2xx": 106907, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 92, "p99_us": 120, "max_us": 4352}
+{"round": 1, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11412, "completed": 114123, "2xx": 114123, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 124, "max_us": 5632}
+{"round": 2, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11523, "completed": 115235, "2xx": 115235, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 112, "max_us": 5888}
+{"round": 2, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10774, "completed": 107740, "2xx": 107740, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 88, "p99_us": 120, "max_us": 4608}
+{"round": 3, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10525, "completed": 105249, "2xx": 105249, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 96, "p99_us": 120, "max_us": 4864}
+{"round": 3, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11493, "completed": 114934, "2xx": 114934, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 112, "max_us": 5888}
+{"round": 4, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11371, "completed": 113709, "2xx": 113709, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 116, "max_us": 4608}
+{"round": 4, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 10925, "completed": 109252, "2xx": 109252, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 6144}
+{"round": 5, "way": "quiet", "items": 1048576, "next": "items-wide", "changes": 0, "requests_per_second": 11110, "completed": 111099, "2xx": 111099, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 84, "p99_us": 116, "max_us": 3968}
+{"round": 5, "way": "changing", "items": 1048576, "next": "items-wide", "changes": 18, "requests_per_second": 11389, "completed": 113893, "2xx": 113893, "4xx": 0, "5xx": 0, "errors": 0, "p50_us": 80, "p99_us": 120, "max_us": 5632}
+{"items": 1048576, "next": "items-wide", "every_s": 0.4, "quiet_p99_us": 120, "changing_p99_us": 116, "ratio": 0.97}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-load.txt b/research/experiments/030-app-lifecycle/2026-10-04-load.txt
new file mode 100644
index 0000000..5581ca3
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-load.txt
@@ -0,0 +1,12 @@
+1 before 10:45:44 up 7:07, 1 user, load average: 0.86, 0.74, 0.72
+1 in-place 10:47:11 up 7:08, 1 user, load average: 1.32, 0.98, 0.82
+2 in-place 10:48:37 up 7:10, 1 user, load average: 1.43, 1.17, 0.91
+2 before 10:50:04 up 7:11, 1 user, load average: 1.26, 1.20, 0.95
+3 before 10:51:50 up 7:13, 1 user, load average: 1.36, 1.36, 1.04
+3 in-place 10:53:47 up 7:15, 1 user, load average: 1.39, 1.50, 1.14
+4 in-place 10:55:20 up 7:16, 1 user, load average: 1.43, 1.54, 1.19
+4 before 10:56:47 up 7:18, 1 user, load average: 1.47, 1.60, 1.25
+5 before 10:58:24 up 7:19, 1 user, load average: 1.37, 1.59, 1.28
+5 in-place 10:59:50 up 7:21, 1 user, load average: 1.29, 1.55, 1.29
+6 in-place 11:01:17 up 7:22, 1 user, load average: 1.45, 1.58, 1.33
+6 before 11:02:43 up 7:24, 1 user, load average: 1.44, 1.60, 1.36
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl
new file mode 100644
index 0000000..ab87451
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-memory.jsonl
@@ -0,0 +1,2 @@
+{"way": "module", "changes": 1000, "private_dirty_kib": [3120, 21952], "pss_kib": [3539, 22371], "per_change_bytes": 19284}
+{"way": "budget", "changes": 1000, "private_dirty_kib": [1200, 1200], "pss_kib": [1619, 1619], "per_change_bytes": 0}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl
new file mode 100644
index 0000000..b43320e
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-1.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.34, "pure_fastest_ms": 5.27, "sqlite_median_ms": 44.91, "sqlite_fastest_ms": 44.79, "sqlite_indexed_median_ms": 14.09, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.84, "pure_fastest_ms": 0.82, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.85, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.52, "pure_fastest_ms": 0.49, "sqlite_median_ms": 1.96, "sqlite_fastest_ms": 1.95, "sqlite_indexed_median_ms": 1.47, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 7.5, "pure_fastest_ms": 6.7, "sqlite_median_ms": 2833.76, "sqlite_fastest_ms": 2831.26, "sqlite_indexed_median_ms": 7.6, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl
new file mode 100644
index 0000000..bbc3032
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-2.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.22, "sqlite_median_ms": 45.34, "sqlite_fastest_ms": 45.13, "sqlite_indexed_median_ms": 14.06, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.84, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.73, "pure_fastest_ms": 6.69, "sqlite_median_ms": 2834.04, "sqlite_fastest_ms": 2830.76, "sqlite_indexed_median_ms": 7.62, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl
new file mode 100644
index 0000000..723657d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-3.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.26, "pure_fastest_ms": 5.19, "sqlite_median_ms": 44.89, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 13.98, "sqlite_indexed_fastest_ms": 13.96}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.9, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.85, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.78, "pure_fastest_ms": 6.66, "sqlite_median_ms": 2835.53, "sqlite_fastest_ms": 2831.69, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl
new file mode 100644
index 0000000..dda11e2
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-4.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.41, "pure_fastest_ms": 5.32, "sqlite_median_ms": 45.21, "sqlite_fastest_ms": 45.1, "sqlite_indexed_median_ms": 14.07, "sqlite_indexed_fastest_ms": 14.03}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.88, "pure_fastest_ms": 0.83, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.42, "pure_fastest_ms": 0.4, "sqlite_median_ms": 1.49, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.83, "pure_fastest_ms": 6.77, "sqlite_median_ms": 2835.74, "sqlite_fastest_ms": 2831.33, "sqlite_indexed_median_ms": 7.57, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl
new file mode 100644
index 0000000..8438db7
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-5.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.3, "pure_fastest_ms": 5.22, "sqlite_median_ms": 44.91, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 14.13, "sqlite_indexed_fastest_ms": 14.1}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.85, "pure_fastest_ms": 0.82, "sqlite_median_ms": 5.87, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.49, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.88, "pure_fastest_ms": 6.77, "sqlite_median_ms": 2839.51, "sqlite_fastest_ms": 2833.02, "sqlite_indexed_median_ms": 7.56, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl
new file mode 100644
index 0000000..6a6a729
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-before-6.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.1, "pure_fastest_ms": 5.05, "sqlite_median_ms": 44.93, "sqlite_fastest_ms": 44.77, "sqlite_indexed_median_ms": 14.1, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.85, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.88, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.74, "pure_fastest_ms": 6.67, "sqlite_median_ms": 2838.12, "sqlite_fastest_ms": 2835.12, "sqlite_indexed_median_ms": 7.58, "sqlite_indexed_fastest_ms": 7.52}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl
new file mode 100644
index 0000000..4cabca4
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-1.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.31, "pure_fastest_ms": 5.25, "sqlite_median_ms": 45.44, "sqlite_fastest_ms": 45.18, "sqlite_indexed_median_ms": 14.09, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.91, "pure_fastest_ms": 0.83, "sqlite_median_ms": 5.95, "sqlite_fastest_ms": 5.89, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.39, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.46, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.46}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.85, "pure_fastest_ms": 6.76, "sqlite_median_ms": 2833.97, "sqlite_fastest_ms": 2829.69, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.52}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl
new file mode 100644
index 0000000..c4fb377
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-2.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.24, "sqlite_median_ms": 44.87, "sqlite_fastest_ms": 44.8, "sqlite_indexed_median_ms": 14.08, "sqlite_indexed_fastest_ms": 14.05}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.82, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.89, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.75, "pure_fastest_ms": 6.7, "sqlite_median_ms": 2835.9, "sqlite_fastest_ms": 2833.26, "sqlite_indexed_median_ms": 7.55, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl
new file mode 100644
index 0000000..054658b
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-3.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.38, "pure_fastest_ms": 5.26, "sqlite_median_ms": 45.3, "sqlite_fastest_ms": 45.05, "sqlite_indexed_median_ms": 13.99, "sqlite_indexed_fastest_ms": 13.93}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.81, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.87, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.37, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.47, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.47, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.76, "pure_fastest_ms": 6.74, "sqlite_median_ms": 2836.89, "sqlite_fastest_ms": 2832.35, "sqlite_indexed_median_ms": 7.58, "sqlite_indexed_fastest_ms": 7.49}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl
new file mode 100644
index 0000000..1a53621
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-4.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.3, "pure_fastest_ms": 5.23, "sqlite_median_ms": 45.78, "sqlite_fastest_ms": 45.2, "sqlite_indexed_median_ms": 14.05, "sqlite_indexed_fastest_ms": 14.01}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 1.0, "pure_fastest_ms": 0.85, "sqlite_median_ms": 5.9, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.36, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.8, "pure_fastest_ms": 6.72, "sqlite_median_ms": 2838.86, "sqlite_fastest_ms": 2835.33, "sqlite_indexed_median_ms": 7.59, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl
new file mode 100644
index 0000000..507eac9
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-5.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.32, "pure_fastest_ms": 5.24, "sqlite_median_ms": 45.19, "sqlite_fastest_ms": 45.04, "sqlite_indexed_median_ms": 14.05, "sqlite_indexed_fastest_ms": 14.02}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.8, "sqlite_median_ms": 5.89, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.38, "pure_fastest_ms": 0.36, "sqlite_median_ms": 1.48, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.71, "pure_fastest_ms": 6.64, "sqlite_median_ms": 2834.72, "sqlite_fastest_ms": 2831.67, "sqlite_indexed_median_ms": 7.71, "sqlite_indexed_fastest_ms": 7.5}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl
new file mode 100644
index 0000000..dd21361
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-queries-in-place-6.jsonl
@@ -0,0 +1,4 @@
+{"query": "per_branch", "rows": 10, "accounts": 100000, "pure_median_ms": 5.23, "pure_fastest_ms": 5.13, "sqlite_median_ms": 45.19, "sqlite_fastest_ms": 45.06, "sqlite_indexed_median_ms": 14.12, "sqlite_indexed_fastest_ms": 14.07}
+{"query": "in_credit", "rows": 1, "accounts": 100000, "pure_median_ms": 0.83, "pure_fastest_ms": 0.79, "sqlite_median_ms": 5.88, "sqlite_fastest_ms": 5.86, "sqlite_indexed_median_ms": 0.16, "sqlite_indexed_fastest_ms": 0.16}
+{"query": "large", "rows": 212, "accounts": 100000, "pure_median_ms": 0.4, "pure_fastest_ms": 0.37, "sqlite_median_ms": 1.5, "sqlite_fastest_ms": 1.47, "sqlite_indexed_median_ms": 1.48, "sqlite_indexed_fastest_ms": 1.47}
+{"query": "ranks", "rows": 404, "accounts": 100000, "pure_median_ms": 6.7, "pure_fastest_ms": 6.63, "sqlite_median_ms": 2835.61, "sqlite_fastest_ms": 2833.18, "sqlite_indexed_median_ms": 7.56, "sqlite_indexed_fastest_ms": 7.51}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl
new file mode 100644
index 0000000..ac6ece1
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-1.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.61, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 41908, "new_orders_per_minute": 1121396, "undone_new_orders": 3571, "errors": 0, "new_order": {"done": 377370, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "payment": {"done": 360166, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "order_status": {"done": 33677, "failed": 0, "p50_us": 256, "p99_us": 1472}, "delivery": {"done": 33572, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "stock_level": {"done": 33380, "failed": 0, "p50_us": 288, "p99_us": 1600}, "server_threads": "3", "cpu_us_per_transaction": 44.2, "core_shares": {"pure-owner": 0.88, "pure-checkpoint": 0.01}, "resident_mib": 441.5, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 441.5, "pure_state_mib": 224.1, "pure_store_mib": 230.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25495, "burst_events": 767622, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl
new file mode 100644
index 0000000..53ff411
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-2.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.57, "pure_load_s": 0.72, "sqlite_load_s": 1.57, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 40884, "new_orders_per_minute": 1093955, "undone_new_orders": 3508, "errors": 0, "new_order": {"done": 368160, "failed": 0, "p50_us": 1536, "p99_us": 4352}, "payment": {"done": 351436, "failed": 0, "p50_us": 1472, "p99_us": 4352}, "order_status": {"done": 32835, "failed": 0, "p50_us": 272, "p99_us": 1472}, "delivery": {"done": 32724, "failed": 0, "p50_us": 1472, "p99_us": 4096}, "stock_level": {"done": 32531, "failed": 0, "p50_us": 304, "p99_us": 1600}, "server_threads": "3", "cpu_us_per_transaction": 45.1, "core_shares": {"pure-owner": 0.87, "pure-checkpoint": 0.01}, "resident_mib": 432.0, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 432.0, "pure_state_mib": 219.2, "pure_store_mib": 222.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25009, "burst_events": 748898, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl
new file mode 100644
index 0000000..cf33a75
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-3.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42900, "new_orders_per_minute": 1147828, "undone_new_orders": 3658, "errors": 0, "new_order": {"done": 386268, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "payment": {"done": 368740, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "order_status": {"done": 34439, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 34372, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "stock_level": {"done": 34175, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.0, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 449.7, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 449.7, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26093, "burst_events": 785803, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl
new file mode 100644
index 0000000..4bf8c0c
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-4.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.73, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42901, "new_orders_per_minute": 1147777, "undone_new_orders": 3672, "errors": 0, "new_order": {"done": 386265, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 368757, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "order_status": {"done": 34442, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34374, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "stock_level": {"done": 34187, "failed": 0, "p50_us": 304, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.7, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 450.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 450.2, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26389, "burst_events": 785808, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl
new file mode 100644
index 0000000..131a025
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-5.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.74, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42976, "new_orders_per_minute": 1149663, "undone_new_orders": 3673, "errors": 0, "new_order": {"done": 386895, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 369465, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "order_status": {"done": 34484, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34429, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "stock_level": {"done": 34244, "failed": 0, "p50_us": 288, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.6, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 451.8, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 451.8, "pure_state_mib": 229.2, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26247, "burst_events": 787183, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl
new file mode 100644
index 0000000..98d082d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-before-6.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43358, "new_orders_per_minute": 1159818, "undone_new_orders": 3705, "errors": 0, "new_order": {"done": 390316, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "payment": {"done": 372786, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "order_status": {"done": 34772, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 34732, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "stock_level": {"done": 34568, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.0, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 454.9, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 454.9, "pure_state_mib": 231.1, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26292, "burst_events": 794213, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl
new file mode 100644
index 0000000..3d0a8ff
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-1.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.73, "sqlite_load_s": 1.56, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43908, "new_orders_per_minute": 1174706, "undone_new_orders": 3766, "errors": 0, "new_order": {"done": 395335, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "payment": {"done": 377415, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "order_status": {"done": 35217, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 35180, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "stock_level": {"done": 35012, "failed": 0, "p50_us": 288, "p99_us": 1216}, "server_threads": "3", "cpu_us_per_transaction": 44.4, "core_shares": {"pure-owner": 0.91, "pure-checkpoint": 0.01}, "resident_mib": 459.9, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 459.9, "pure_state_mib": 233.7, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26826, "burst_events": 804248, "checkpoints": 39}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl
new file mode 100644
index 0000000..cecc96a
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-2.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43426, "new_orders_per_minute": 1161961, "undone_new_orders": 3714, "errors": 0, "new_order": {"done": 391037, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "payment": {"done": 373282, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "order_status": {"done": 34830, "failed": 0, "p50_us": 256, "p99_us": 1280}, "delivery": {"done": 34765, "failed": 0, "p50_us": 1472, "p99_us": 3712}, "stock_level": {"done": 34611, "failed": 0, "p50_us": 304, "p99_us": 1344}, "server_threads": "3", "cpu_us_per_transaction": 44.1, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 455.0, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 455.0, "pure_state_mib": 231.5, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26610, "burst_events": 795456, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl
new file mode 100644
index 0000000..97318be
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-3.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.64, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 42899, "new_orders_per_minute": 1147829, "undone_new_orders": 3662, "errors": 0, "new_order": {"done": 386274, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "payment": {"done": 368750, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "order_status": {"done": 34423, "failed": 0, "p50_us": 256, "p99_us": 1216}, "delivery": {"done": 34332, "failed": 0, "p50_us": 1472, "p99_us": 3584}, "stock_level": {"done": 34209, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 44.5, "core_shares": {"pure-owner": 0.89, "pure-checkpoint": 0.01}, "resident_mib": 450.4, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 450.4, "pure_state_mib": 228.9, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25746, "burst_events": 785777, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl
new file mode 100644
index 0000000..e7978fd
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-4.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.58, "pure_load_s": 0.73, "sqlite_load_s": 1.6, "sqlite_page_size": 4096, "pure_resident_mib": 39.0, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 41272, "new_orders_per_minute": 1104070, "undone_new_orders": 3527, "errors": 0, "new_order": {"done": 371551, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "payment": {"done": 354807, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "order_status": {"done": 33181, "failed": 0, "p50_us": 272, "p99_us": 1600}, "delivery": {"done": 33061, "failed": 0, "p50_us": 1472, "p99_us": 4608}, "stock_level": {"done": 32844, "failed": 0, "p50_us": 304, "p99_us": 1856}, "server_threads": "3", "cpu_us_per_transaction": 45.1, "core_shares": {"pure-owner": 0.87, "pure-checkpoint": 0.01}, "resident_mib": 436.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 436.2, "pure_state_mib": 221.0, "pure_store_mib": 230.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 25301, "burst_events": 755969, "checkpoints": 37}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl
new file mode 100644
index 0000000..1c2444d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-5.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.58, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 43015, "new_orders_per_minute": 1150991, "undone_new_orders": 3677, "errors": 0, "new_order": {"done": 387341, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "payment": {"done": 369725, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "order_status": {"done": 34509, "failed": 0, "p50_us": 256, "p99_us": 1344}, "delivery": {"done": 34443, "failed": 0, "p50_us": 1472, "p99_us": 3840}, "stock_level": {"done": 34277, "failed": 0, "p50_us": 304, "p99_us": 1408}, "server_threads": "3", "cpu_us_per_transaction": 44.4, "core_shares": {"pure-owner": 0.89, "pure-checkpoint": 0.01}, "resident_mib": 451.2, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 451.2, "pure_state_mib": 229.5, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26384, "burst_events": 787918, "checkpoints": 38}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl
new file mode 100644
index 0000000..8980002
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-tpcc-in-place-6.jsonl
@@ -0,0 +1,3 @@
+{"step": "load", "directory": "/dev/shm", "warehouses": 1, "rows": 568758, "generate_s": 0.56, "pure_load_s": 0.72, "sqlite_load_s": 1.57, "sqlite_page_size": 4096, "pure_resident_mib": 38.3, "sqlite_file_mib": 13.8}
+{"step": "run", "system": "pure", "warehouses": 1, "terminals": 64, "transactions_per_second": 44046, "new_orders_per_minute": 1178462, "undone_new_orders": 3781, "errors": 0, "new_order": {"done": 396614, "failed": 0, "p50_us": 1472, "p99_us": 3456}, "payment": {"done": 378619, "failed": 0, "p50_us": 1408, "p99_us": 3328}, "order_status": {"done": 35311, "failed": 0, "p50_us": 256, "p99_us": 1152}, "delivery": {"done": 35287, "failed": 0, "p50_us": 1472, "p99_us": 3328}, "stock_level": {"done": 35114, "failed": 0, "p50_us": 288, "p99_us": 1280}, "server_threads": "3", "cpu_us_per_transaction": 43.8, "core_shares": {"pure-owner": 0.9, "pure-checkpoint": 0.01}, "resident_mib": 460.6, "written_bytes_per_transaction": 0.0, "device_bytes_per_transaction": null, "consistency_failures": []}
+{"step": "end", "pure_resident_mib": 460.6, "pure_state_mib": 234.4, "pure_store_mib": 238.7, "sqlite_files_mib": 13.8, "grouped_sqlite_files_mib": 0.0, "pure_counts": {"bursts": 26765, "burst_events": 806824, "checkpoints": 39}}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl
new file mode 100644
index 0000000..fa51827
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-next.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.44, "reading_ms": 0.16}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.92, "reading_ms": 0.23}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.54, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.38, "reading_ms": 0.24}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.15, "reading_ms": 1.24}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.09, "reading_ms": 1.2}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.16, "reading_ms": 1.25}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.09, "reading_ms": 1.2}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.53, "reading_ms": 0.23}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.65, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 3.19, "reading_ms": 0.28}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.39, "reading_ms": 0.25}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.06, "reading_ms": 0.26}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.2, "reading_ms": 1.24}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.47, "reading_ms": 0.28}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.35, "reading_ms": 0.22}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.05, "reading_ms": 1.21}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.53, "reading_ms": 0.21}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 2.15, "reading_ms": 0.2}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.92, "reading_ms": 0.16}
+{"items": 1024, "way": "restarted", "gap_ms": 2.77}
+{"items": 1024, "way": "restarted", "gap_ms": 2.32}
+{"items": 1024, "way": "restarted", "gap_ms": 2.7}
+{"items": 1024, "way": "restarted", "gap_ms": 2.32}
+{"items": 1024, "way": "restarted", "gap_ms": 2.53}
+{"items": 1024, "way": "restarted", "gap_ms": 2.35}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.72}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "changed_median_ms": 1.06, "restarted_median_ms": 2.42, "refused": 63, "answered": 66300}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl
new file mode 100644
index 0000000..4fbda76
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000-items-wide.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 2.76, "reading_ms": 0.39}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.08, "reading_ms": 4.92}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.61, "reading_ms": 0.37}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.34, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.37, "reading_ms": 1.45}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.29, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.32, "reading_ms": 1.43}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.27, "reading_ms": 1.4}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.44, "reading_ms": 0.38}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.35, "reading_ms": 1.43}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.74, "reading_ms": 0.34}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.34, "reading_ms": 0.34}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 0.49, "reading_ms": 0.44}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 2.15, "reading_ms": 0.37}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.74, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.6, "reading_ms": 0.32}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.94, "reading_ms": 0.43}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.4, "reading_ms": 0.47}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 1.3, "reading_ms": 1.41}
+{"items": 1024, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.3, "reading_ms": 1.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.88}
+{"items": 1024, "way": "restarted", "gap_ms": 2.7}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "way": "restarted", "gap_ms": 2.8}
+{"items": 1024, "way": "restarted", "gap_ms": 2.54}
+{"items": 1024, "way": "restarted", "gap_ms": 2.52}
+{"items": 1024, "way": "restarted", "gap_ms": 2.49}
+{"items": 1024, "way": "restarted", "gap_ms": 2.47}
+{"items": 1024, "way": "restarted", "gap_ms": 2.43}
+{"items": 1024, "changed_median_ms": 1.31, "restarted_median_ms": 2.51, "refused": 130, "answered": 66485}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl
new file mode 100644
index 0000000..7ceccc0
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-next.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.41, "reading_ms": 0.3}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 3.91, "reading_ms": 1.37}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 0.54, "reading_ms": 0.25}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.43, "reading_ms": 0.22}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.1, "reading_ms": 1.24}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 1.19, "reading_ms": 1.28}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.14, "reading_ms": 1.21}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 4.13, "reading_ms": 0.21}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "changed", "gap_ms": 1.92, "reading_ms": 0.22}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 0.3, "reading_ms": 0.22}
+{"items": 1048576, "way": "restarted", "gap_ms": 151.32}
+{"items": 1048576, "way": "restarted", "gap_ms": 150.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.28}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.94}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.65}
+{"items": 1048576, "changed_median_ms": 1.12, "restarted_median_ms": 149.94, "refused": 45, "answered": 34645}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl
new file mode 100644
index 0000000..3b08f1c
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-in-place-1000000-items-wide.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 178.42, "reading_ms": 175.85}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 160.31, "reading_ms": 157.02}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 177.96, "reading_ms": 175.48}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 161.7, "reading_ms": 158.05}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 181.63, "reading_ms": 178.71}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 163.76, "reading_ms": 159.67}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 182.44, "reading_ms": 178.78}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 163.71, "reading_ms": 159.36}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "changed", "gap_ms": 185.21, "reading_ms": 180.69}
+{"items": 1048576, "way": "changed", "to": "items", "did": "changed", "gap_ms": 165.13, "reading_ms": 160.48}
+{"items": 1048576, "way": "restarted", "gap_ms": 332.42}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.08}
+{"items": 1048576, "way": "restarted", "gap_ms": 327.97}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.43}
+{"items": 1048576, "way": "restarted", "gap_ms": 328.56}
+{"items": 1048576, "changed_median_ms": 171.54, "restarted_median_ms": 332.42, "refused": 20534, "answered": 34071}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl
new file mode 100644
index 0000000..15c59fb
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-next.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.5, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.53, "reading_ms": 1.78}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 0.55, "reading_ms": 0.57}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.51, "reading_ms": 1.6}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.7, "reading_ms": 0.68}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 0.78, "reading_ms": 0.54}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 3.99, "reading_ms": 0.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.48, "reading_ms": 1.61}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.6, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.54, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.67}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.49, "reading_ms": 1.63}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.3, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.51, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 1.54, "reading_ms": 1.69}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.56, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 3.08, "reading_ms": 0.71}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.49, "reading_ms": 1.64}
+{"items": 1024, "way": "restarted", "gap_ms": 2.49}
+{"items": 1024, "way": "restarted", "gap_ms": 2.35}
+{"items": 1024, "way": "restarted", "gap_ms": 2.45}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "way": "restarted", "gap_ms": 3.89}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "way": "restarted", "gap_ms": 2.54}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.42}
+{"items": 1024, "changed_median_ms": 1.52, "restarted_median_ms": 2.42, "refused": 253, "answered": 64398}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl
new file mode 100644
index 0000000..81f153d
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000-items-wide.jsonl
@@ -0,0 +1,31 @@
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 3.4, "reading_ms": 0.77}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.5, "reading_ms": 0.86}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 4.02, "reading_ms": 1.7}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 5.3, "reading_ms": 1.79}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.66, "reading_ms": 0.66}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 0.59, "reading_ms": 0.63}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 4.01, "reading_ms": 0.6}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 0.52, "reading_ms": 0.61}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.52, "reading_ms": 1.64}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 2.04, "reading_ms": 1.68}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.58, "reading_ms": 1.68}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 2.23, "reading_ms": 0.7}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 2.37, "reading_ms": 0.58}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.69}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.53, "reading_ms": 0.59}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.31, "reading_ms": 0.58}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 0.94, "reading_ms": 0.6}
+{"items": 1024, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 1.57, "reading_ms": 1.66}
+{"items": 1024, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 1.61, "reading_ms": 1.65}
+{"items": 1024, "way": "restarted", "gap_ms": 2.73}
+{"items": 1024, "way": "restarted", "gap_ms": 2.44}
+{"items": 1024, "way": "restarted", "gap_ms": 2.36}
+{"items": 1024, "way": "restarted", "gap_ms": 3.84}
+{"items": 1024, "way": "restarted", "gap_ms": 2.66}
+{"items": 1024, "way": "restarted", "gap_ms": 2.8}
+{"items": 1024, "way": "restarted", "gap_ms": 2.41}
+{"items": 1024, "way": "restarted", "gap_ms": 2.39}
+{"items": 1024, "way": "restarted", "gap_ms": 2.63}
+{"items": 1024, "way": "restarted", "gap_ms": 4.49}
+{"items": 1024, "changed_median_ms": 1.58, "restarted_median_ms": 2.64, "refused": 173, "answered": 62631}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl
new file mode 100644
index 0000000..23d81fd
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-next.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 151.25, "reading_ms": 151.2}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 149.93, "reading_ms": 150.0}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 150.36, "reading_ms": 150.34}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 151.27, "reading_ms": 151.31}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.72, "reading_ms": 149.74}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 150.48, "reading_ms": 150.5}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.59, "reading_ms": 149.58}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 150.75, "reading_ms": 150.88}
+{"items": 1048576, "way": "changed", "to": "items-next", "did": "restarted", "gap_ms": 149.79, "reading_ms": 149.8}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 153.03, "reading_ms": 153.06}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 148.91}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.63}
+{"items": 1048576, "way": "restarted", "gap_ms": 149.21}
+{"items": 1048576, "way": "restarted", "gap_ms": 151.98}
+{"items": 1048576, "changed_median_ms": 150.42, "restarted_median_ms": 149.63, "refused": 18133, "answered": 33199}
diff --git a/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl
new file mode 100644
index 0000000..07a3b26
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/2026-10-04-window-stop-start-1000000-items-wide.jsonl
@@ -0,0 +1,16 @@
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 329.24, "reading_ms": 329.21}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 339.41, "reading_ms": 339.48}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 335.25, "reading_ms": 335.32}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 345.5, "reading_ms": 345.51}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 328.75, "reading_ms": 328.83}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 342.24, "reading_ms": 342.22}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 328.17, "reading_ms": 328.22}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 343.68, "reading_ms": 343.74}
+{"items": 1048576, "way": "changed", "to": "items-wide", "did": "restarted", "gap_ms": 332.61, "reading_ms": 332.6}
+{"items": 1048576, "way": "changed", "to": "items", "did": "restarted", "gap_ms": 340.55, "reading_ms": 339.73}
+{"items": 1048576, "way": "restarted", "gap_ms": 329.85}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.79}
+{"items": 1048576, "way": "restarted", "gap_ms": 327.33}
+{"items": 1048576, "way": "restarted", "gap_ms": 337.1}
+{"items": 1048576, "way": "restarted", "gap_ms": 329.47}
+{"items": 1048576, "changed_median_ms": 337.33, "restarted_median_ms": 329.85, "refused": 40844, "answered": 33921}
diff --git a/research/experiments/030-app-lifecycle/isolation.py b/research/experiments/030-app-lifecycle/isolation.py
new file mode 100644
index 0000000..43259f4
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/isolation.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""Another app's latency while one app changes again and again on a running
+host (docs/design/017's stage 3, research 115).
+
+A host of apps (two connection threads, two workers, stores in RAM, cores
+0-2) holds A, an app of items.aup with ITEMS items, and B, the scoreboard
+(benchmarks/http/scores.aup). B takes one connection's scores
+(benchmarks/http/load on core 3) for SECONDS, while A is left be (quiet)
+or changes every EVERY seconds (changing: SIGHUP, its line naming in turn
+items.pmod and NEXT). The two take turns, their order turned each round.
+
+isolation.py PURE_SERVE ITEMS NEXT EVERY SECONDS ROUNDS a JSON line per run,
+ then one with medians
+"""
+import json
+import select
+import shutil
+import signal
+import statistics
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from pathlib import Path
+
+from window import AUP, HERE, grow
+
+ROOT = HERE.parents[2]
+LOAD = ROOT / 'benchmarks' / 'http' / 'load'
+
+
+def main():
+ serve, items, after = Path(sys.argv[1]).resolve(), int(sys.argv[2]), sys.argv[3]
+ every, seconds, rounds = float(sys.argv[4]), int(sys.argv[5]), int(sys.argv[6])
+ work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
+ try:
+ modules = {}
+ for name, source in (('items', HERE / 'items.aup'), (after, HERE / f'{after}.aup'),
+ ('scores', ROOT / 'benchmarks' / 'http' / 'scores.aup')):
+ modules[name] = work / f'{name}.pmod'
+ subprocess.run([str(AUP), 'build', str(source), '-o', str(modules[name])], check=True)
+ conf = work / 'apps.conf'
+
+ def write(module):
+ conf.write_text(f"a {modules[module]} {work / 'a'}\nb {modules['scores']} {work / 'b'}\n")
+
+ write('items')
+ host = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--apps', str(conf), '--port', '0', '--threads', '2',
+ '--workers', '2'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ port = json.loads(host.stdout.readline())['port']
+ count = grow(port, items, 'a')
+ p99 = {'quiet': [], 'changing': []}
+ changes = 0
+ for round_ in range(1, rounds + 1):
+ for way in (['quiet', 'changing'] if round_ % 2 else ['changing', 'quiet']):
+ load = subprocess.Popen(['taskset', '-c', '3', str(LOAD), str(port), '1', str(seconds), 'score', '1',
+ 'keep-alive', '16', '/b'], stdout=subprocess.PIPE, text=True)
+ made = 0
+ while way == 'changing' and load.poll() is None:
+ write(after if changes % 2 == 0 else 'items')
+ host.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.stdout], [], [], 60)
+ assert ready
+ said = json.loads(host.stdout.readline())
+ assert said['apps'][0]['running'], said
+ changes += 1
+ made += 1
+ time.sleep(every)
+ out, _ = load.communicate()
+ result = json.loads(out)
+ assert result['5xx'] == 0 and result['errors'] == 0, result
+ p99[way].append(result['p99_us'])
+ print(json.dumps({'round': round_, 'way': way, 'items': count, 'next': after, 'changes': made, **result}),
+ flush=True)
+ host.terminate()
+ host.communicate(timeout=600)
+ quiet, changing = statistics.median(p99['quiet']), statistics.median(p99['changing'])
+ print(json.dumps({'items': count, 'next': after, 'every_s': every, 'quiet_p99_us': quiet,
+ 'changing_p99_us': changing, 'ratio': round(changing / quiet, 2)}), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/research/experiments/030-app-lifecycle/items-wide.aup b/research/experiments/030-app-lifecycle/items-wide.aup
new file mode 100644
index 0000000..6b28477
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/items-wide.aup
@@ -0,0 +1,29 @@
+record Item {
+ n = 0
+ m = 0
+}
+state items: list of Item
+state more: list of Item
+state hits = 0
+input seed = message { n: number }
+input grow = message { step: number }
+input add = message { step: number }
+on seed {
+ items.append(Item { n: seed.n })
+}
+on grow {
+ for i in items {
+ more.append(Item { n: i.n + grow.step })
+ }
+ for m in more {
+ items.append(Item { n: m.n })
+ }
+ for m in more {
+ more.remove(m)
+ }
+}
+on add {
+ hits = hits + add.step
+}
+view count = count(i in items where i.n > 0)
+view total = hits
diff --git a/research/experiments/030-app-lifecycle/memory.py b/research/experiments/030-app-lifecycle/memory.py
new file mode 100644
index 0000000..3c15d4a
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/memory.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""What changing an app leaves in a running host's memory (docs/design/017's
+stage 3, research 115). A host of apps holds an app of items.aup (a
+thousand items, its store in RAM); its own memory (smaps_rollup's
+Private_Dirty and Pss) is read after a warm-up of changes, then after
+CHANGES changes more: in place to another module (its line naming in turn
+items.pmod and items-next.pmod: each reading loads a module, and keeps the
+one it replaced), and with its module as it was (its queue budget changed
+in turn: no module loaded). The difference a change.
+
+memory.py PURE_SERVE CHANGES a JSON line per way
+"""
+import json
+import select
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+from window import AUP, HERE, grow
+
+
+def own(pid):
+ fields = {}
+ for line in Path(f'/proc/{pid}/smaps_rollup').read_text().splitlines()[1:]:
+ name, value = line.split(':', 1)
+ fields[name] = int(value.split()[0])
+ return fields['Private_Dirty'], fields['Pss']
+
+
+def main():
+ serve, changes = Path(sys.argv[1]).resolve(), int(sys.argv[2])
+ work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
+ try:
+ modules = {}
+ for name in ('items', 'items-next'):
+ modules[name] = work / f'{name}.pmod'
+ subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True)
+ for way in ('module', 'budget'):
+ conf = work / 'apps.conf'
+
+ def write(n):
+ module = 'items-next' if way == 'module' and n % 2 else 'items'
+ budget = f' queue={1000 + n % 2}' if way == 'budget' else ''
+ conf.write_text(f"x {modules[module]} {work / way}{budget}\n")
+
+ write(0)
+ host = subprocess.Popen([str(serve), '--apps', str(conf), '--port', '0'], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ port = json.loads(host.stdout.readline())['port']
+ grow(port, 1000)
+
+ def change(n):
+ write(n)
+ host.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.stdout], [], [], 60)
+ assert ready
+ said = json.loads(host.stdout.readline())
+ assert said['apps'][0]['did'] == 'changed' and said['apps'][0]['running'], said
+
+ for n in range(1, 101): # warm: the allocator's arenas settle
+ change(n)
+ dirty, pss = own(host.pid)
+ for n in range(101, 101 + changes):
+ change(n)
+ dirty_after, pss_after = own(host.pid)
+ host.terminate()
+ host.communicate(timeout=60)
+ print(json.dumps({'way': way, 'changes': changes, 'private_dirty_kib': [dirty, dirty_after],
+ 'pss_kib': [pss, pss_after],
+ 'per_change_bytes': round((dirty_after - dirty) * 1024 / changes)}), flush=True)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/research/experiments/030-app-lifecycle/pairs.py b/research/experiments/030-app-lifecycle/pairs.py
new file mode 100644
index 0000000..2ea4f48
--- /dev/null
+++ b/research/experiments/030-app-lifecycle/pairs.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""The pairs of runs.sh pairs as a table (research 115): each round's TPC-C
+(transactions a second, CPU a transaction, the owner's core share) before
+and after, the medians and the bank queries' median times.
+
+pairs.py DATE from DATE-tpcc-*.jsonl and DATE-queries-*.jsonl beside it
+"""
+import json
+import statistics
+import sys
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+day = sys.argv[1]
+
+
+def tpcc(version, round_):
+ for line in (HERE / f'{day}-tpcc-{version}-{round_}.jsonl').read_text().splitlines():
+ r = json.loads(line)
+ if r.get('step') == 'run' and r.get('system') == 'pure':
+ owner = max((share for name, share in r['core_shares'].items() if 'owner' in name), default=None) \
+ if isinstance(r['core_shares'], dict) else None
+ return r['transactions_per_second'], r['cpu_us_per_transaction'], owner
+
+
+def queries(version, round_):
+ return {r['query']: r['pure_median_ms'] for r in map(json.loads, (HERE / f'{day}-queries-{version}-{round_}.jsonl')
+ .read_text().splitlines()) if 'query' in r}
+
+
+rounds = sorted({int(p.stem.rsplit('-', 1)[1]) for p in HERE.glob(f'{day}-tpcc-before-*.jsonl')})
+rows = {'before': [], 'in-place': []}
+for round_ in rounds:
+ b, a = tpcc('before', round_), tpcc('in-place', round_)
+ rows['before'].append(b), rows['in-place'].append(a)
+ print(json.dumps({'round': round_, 'tps': [b[0], a[0]], 'change': round((a[0] / b[0] - 1) * 100, 1),
+ 'cpu_us': [b[1], a[1]], 'owner_share': [b[2], a[2]]}))
+med = {v: statistics.median(t for t, _, _ in rows[v]) for v in rows}
+cpu = {v: statistics.median(c for _, c, _ in rows[v]) for v in rows}
+names = queries('before', rounds[0]).keys()
+q = {v: {n: statistics.median(queries(v, r)[n] for r in rounds) for n in names} for v in rows}
+print(json.dumps({'tps_median': [med['before'], med['in-place']], 'change': round((med['in-place'] / med['before'] - 1) * 100, 1),
+ 'cpu_us_median': [cpu['before'], cpu['in-place']],
+ 'queries_median_ms': {n: [q['before'][n], q['in-place'][n],
+ round((q['in-place'][n] / q['before'][n] - 1) * 100, 1)] for n in names}}))
diff --git a/research/experiments/030-app-lifecycle/runs.sh b/research/experiments/030-app-lifecycle/runs.sh
index 601f754..0a70921 100755
--- a/research/experiments/030-app-lifecycle/runs.sh
+++ b/research/experiments/030-app-lifecycle/runs.sh
@@ -5,6 +5,15 @@
# in ~/.cache/pure-030/NAME
# runs.sh window NAME ITEMS CHANGES RESTARTS [MODULE] window.py on that
# build, into DATE-window-NAME-ITEMS[-MODULE].jsonl
+# runs.sh isolation NAME ITEMS NEXT EVERY SECONDS ROUNDS isolation.py,
+# into DATE-isolation-NAME-ITEMS-NEXT.jsonl
+# runs.sh pairs FIRST LAST a host of one app, the builds before (e4c7f7e,
+# stage 3's criteria) and after in turns, as
+# 026 ran them: TPC-C in RAM (W=1, 64
+# terminals, 20 s) and the bank's queries
+# runs.sh counts the bank queries' instructions before and
+# after (025's count.py)
+# runs.sh memory CHANGES what changing an app leaves in memory (memory.py)
set -e
out=research/experiments/030-app-lifecycle
builds=$HOME/.cache/pure-030
@@ -21,5 +30,29 @@ window)
until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
python3 research/guard.py --most 2048 -- python3 $out/window.py "$builds/$2/pure-serve" "$3" "$4" "$5" ${6:+"$6"} \
> "$out/$day-window-$2-$3${6:+-$6}.jsonl" ;;
-*) echo "runs.sh build NAME COMMIT | window NAME ITEMS CHANGES RESTARTS [MODULE]" >&2; exit 2 ;;
+pairs)
+ for round in $(seq "$2" "$3"); do
+ first=before second=in-place
+ if [ $((round % 2)) = 0 ]; then first=in-place second=before; fi
+ for version in $first $second; do
+ until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
+ echo "$round $version $(uptime)" >> "$out/$day-load.txt"
+ python3 research/guard.py -- python3 benchmarks/tpcc/run.py --serve "$builds/$version/pure-serve" --warehouses 1 \
+ --check 0 --terminals 64 --sqlite-threads '' --sqlite-group '' --seconds 20 \
+ > "$out/$day-tpcc-$version-$round.jsonl"
+ python3 research/guard.py -- python3 benchmarks/bank/queries.py --serve "$builds/$version/pure-serve" \
+ > "$out/$day-queries-$version-$round.jsonl"
+ done
+ done ;;
+counts)
+ python3 research/guard.py -- python3 research/experiments/025-i64-cost/count.py "$builds/before/pure-serve" \
+ "$builds/in-place/pure-serve" "$builds/before/pure-serve" "$builds/in-place/pure-serve" > "$out/$day-counts.jsonl" ;;
+memory)
+ python3 research/guard.py -- python3 $out/memory.py "$builds/in-place/pure-serve" "$2" > "$out/$day-memory.jsonl" ;;
+isolation)
+ until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
+ python3 research/guard.py --most 2048 -- python3 $out/isolation.py "$builds/$2/pure-serve" "$3" "$4" "$5" "$6" "$7" \
+ > "$out/$day-isolation-$2-$3-$4.jsonl" ;;
+*) echo "runs.sh build NAME COMMIT | window NAME ITEMS CHANGES RESTARTS [MODULE] | isolation NAME ITEMS NEXT EVERY SECONDS ROUNDS | pairs FIRST LAST | counts | memory CHANGES" >&2
+ exit 2 ;;
esac
diff --git a/research/experiments/030-app-lifecycle/window.py b/research/experiments/030-app-lifecycle/window.py
index 3ec21a5..3c1d511 100755
--- a/research/experiments/030-app-lifecycle/window.py
+++ b/research/experiments/030-app-lifecycle/window.py
@@ -7,12 +7,16 @@ of apps, its store in RAM, the host on cores 0-2. A client on core 3 posts
writes to it one after another on one connection; the time an app cannot
answer is the longest gap between two answered writes (200) around the
change. Changed: SIGHUP after its line names the other of two module files
-(items.pmod, items-next.pmod: the same states, a view more), so each
-reading starts it again on another module. Restarted: SIGTERM, then the
-host started again (the client reconnecting at once).
-
-window.py PURE_SERVE ITEMS CHANGES RESTARTS a JSON line per change and per
- restart, then one with medians
+(items.pmod and NEXT, items-next.pmod unless named: the same states, a
+view more; items-wide.pmod: its records a field more, so the list is laid
+out anew), so each reading changes it to another module. Restarted: the
+same change made the way there was before, the line naming the other
+module, then SIGTERM and the host started again (the client reconnecting
+at once).
+
+window.py PURE_SERVE ITEMS CHANGES RESTARTS [NEXT] a JSON line per change
+ and per restart, then one with
+ medians
"""
import http.client
import json
@@ -93,7 +97,7 @@ def free_port():
return probe.getsockname()[1]
-def grow(port, items):
+def grow(port, items, app='x'):
connection = http.client.HTTPConnection('127.0.0.1', port, timeout=600)
def post(path):
@@ -102,10 +106,10 @@ def grow(port, items):
answer.read()
assert answer.status == 200, (path, answer.status)
- post('/x/inputs/seed?n=1')
+ post(f'/{app}/inputs/seed?n=1')
count = 1
while count < items:
- post('/x/inputs/grow?step=1')
+ post(f'/{app}/inputs/grow?step=1')
count *= 2
connection.close()
return count
@@ -113,10 +117,11 @@ def grow(port, items):
def main():
serve, items, changes, restarts = Path(sys.argv[1]).resolve(), int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
+ after = sys.argv[5] if len(sys.argv) > 5 else 'items-next'
work = Path(tempfile.mkdtemp(prefix='pure-030-', dir='/dev/shm'))
try:
modules = {}
- for name in ('items', 'items-next'):
+ for name in ('items', after):
modules[name] = work / f'{name}.pmod'
subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True)
conf, store, port = work / 'apps.conf', work / 'store', free_port()
@@ -132,17 +137,19 @@ def main():
time.sleep(0.5)
gaps = {'changed': [], 'restarted': []}
for n in range(changes):
- write('items-next' if n % 2 == 0 else 'items')
+ write(after if n % 2 == 0 else 'items')
since = time.monotonic()
began = time.perf_counter()
said = host.reading()
took = time.perf_counter() - began
- assert said['apps'][0]['did'] == 'restarted' and said['apps'][0]['running'], said
+ assert said['apps'][0]['did'] in ('restarted', 'changed') and said['apps'][0]['running'], said
time.sleep(0.3)
gaps['changed'].append(writer.gap(since))
- print(json.dumps({'items': count, 'way': 'changed', 'gap_ms': round(gaps['changed'][-1] * 1000, 2),
+ print(json.dumps({'items': count, 'way': 'changed', 'to': after if n % 2 == 0 else 'items',
+ 'did': said['apps'][0]['did'], 'gap_ms': round(gaps['changed'][-1] * 1000, 2),
'reading_ms': round(took * 1000, 2)}), flush=True)
for n in range(restarts):
+ write(after if (changes + n) % 2 == 0 else 'items')
since = time.monotonic()
host.stop()
host = Host(serve, conf, port)