docs: the phone-logging decision, how to use it, and two build-apk traps
DECISIONS.md gets the route and both rejected alternatives with what each would have cost; RUST.md gets a "Phone logging" section with the build command, where to read it on the phone, the end-to-end verification, and the two rig traps that cost an hour -- Gradle's merged-native-libs cache surviving build-apk.sh's `rm -rf jniLibs` (a --abi x86_64 APK packaged arm64 and aborted with what reads exactly like a Vulkan fault), and the 648 MB debug bench APK that cannot be installed at all. IRIS.md gets the client-core logging API with a before/after. Queue item ticked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
896c93a59a
commit
238057ad5e
3 files changed
+189
-5
No files matched your search
@@ -39,6 +39,57 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
|||||||
why a fix (an OEM-specific named-family pin, or patching around
|
why a fix (an OEM-specific named-family pin, or patching around
|
||||||
fontique) is left as a follow-up rather than done in this pass.
|
fontique) is left as a follow-up rather than done in this pass.
|
||||||
|
|
||||||
|
## 2026-09-07 (how a phone log reaches Iris)
|
||||||
|
|
||||||
|
- **The app sends its own log to `ai-server`, and Dev Updater shows it as
|
||||||
|
`ai-server`'s runtime log.** Iris has no `adb`/`logcat` on her phone, and
|
||||||
|
Android forbids one app reading another's logcat, so the app has to carry
|
||||||
|
its own copy and post it somewhere. `POST /client-log` on `ai-server`
|
||||||
|
re-emits each line into that server's own `tracing` output; Dev Updater
|
||||||
|
already runs `ai-server` as a `Managed` component, whose stdout its own
|
||||||
|
service script redirects to a file and reports through
|
||||||
|
`GET /apps/{key}/components/{name}/logs?kind=runtime`, which the phone
|
||||||
|
app's log dialog already offers as a **Runtime** tab for a `server`
|
||||||
|
component. So **no change to Dev Updater at all** -- one route on
|
||||||
|
`ai-server`, and the client in `client-core`.
|
||||||
|
|
||||||
|
**Rejected: posting to Dev Updater's own server** (the first candidate).
|
||||||
|
It would need a new authenticated *write* route on a TLS surface whose
|
||||||
|
module doc says every route on it "is, or decides, the bytes that get
|
||||||
|
handed to `REQUEST_INSTALL_PACKAGES` next"; a per-app device-log store;
|
||||||
|
a change to `component_logs` so an APK component can have a runtime log;
|
||||||
|
a change to the phone app's `hasBothKinds = component.kind == "server"`
|
||||||
|
gate and to what `hasRuntimeLogs` means on the wire; and -- the real
|
||||||
|
cost -- a **second** enrollment for the iris app, since it has no CA or
|
||||||
|
token for Dev Updater and Dev Updater mints tokens per device by QR.
|
||||||
|
Five changes across two repos against one route, for the same line
|
||||||
|
landing in the same viewer.
|
||||||
|
|
||||||
|
**Rejected: a share intent from a debug button** (a log file in the app's
|
||||||
|
external files dir, shared by hand). It works today and needs no server,
|
||||||
|
but every line costs Iris a manual export and a message, which is the
|
||||||
|
round trip through a person this was meant to remove. It is still the
|
||||||
|
fallback when the tunnel is down, and GrapheneOS's own per-app log export
|
||||||
|
already covers the crash case (that is how the `ToolInput.highlighted`
|
||||||
|
crash was reported).
|
||||||
|
|
||||||
|
- **The ring is in `client-core`, not in the Android crate.** A bounded
|
||||||
|
in-memory ring (2000 lines or 256 KiB, whichever bites first) behind a
|
||||||
|
`log::Log` backend that *forwards* to whichever logger the platform
|
||||||
|
already installed, so `logcat` and a desktop terminal see exactly what
|
||||||
|
they saw before. The platform supplies only its own logger and its
|
||||||
|
destination. `Copy report` appends the ring to what goes on the
|
||||||
|
clipboard, and flushes the uploader first.
|
||||||
|
|
||||||
|
- **The destination is baked in at build time, from the build machine's
|
||||||
|
own files** (`AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` plus the pinned CA) --
|
||||||
|
all three or none, never two. The same trust boundary the transcript
|
||||||
|
config and the Compose APK's CA already use: nothing secret is
|
||||||
|
committed, and an APK is good for the server that built it. A build told
|
||||||
|
nothing still keeps its ring and still copies it; the diagnostics pane
|
||||||
|
says which of "not tried yet", "failing -- <why>" and "no server
|
||||||
|
configured" it is, because otherwise all three look like silence.
|
||||||
|
|
||||||
## 2026-09-06 (how a tool call looks, P1b)
|
## 2026-09-06 (how a tool call looks, P1b)
|
||||||
|
|
||||||
- **A card that never got a result says "no result", in yellow, and it is
|
- **A card that never got a result says "no result", in yellow, and it is
|
||||||
|
|||||||
@@ -8,6 +8,43 @@ capability that moved. Small and trivial changes do not go here.
|
|||||||
An entry gives the date, what changed, why, and a short before/after where
|
An entry gives the date, what changed, why, and a short before/after where
|
||||||
it helps judge the change without the session that made it. Newest first.
|
it helps judge the change without the session that made it. Newest first.
|
||||||
|
|
||||||
|
## 2026-09-07: `client-core` carries the app's own log
|
||||||
|
|
||||||
|
Not iris itself but the crate beside it, and it is a new public surface an
|
||||||
|
app author will use: `client_core::log_ring` and `client_core::log_upload`.
|
||||||
|
Because Iris's phone has no `logcat`, an app now keeps a bounded copy of
|
||||||
|
its own log and can send it to `ai-server`, where Dev Updater already
|
||||||
|
shows it.
|
||||||
|
|
||||||
|
Before, an app installed a platform logger and that was the end of it:
|
||||||
|
|
||||||
|
android_logger::init_once(config); // Android
|
||||||
|
// nothing at all on the desktop
|
||||||
|
|
||||||
|
After, the platform's logger becomes the *inner* logger of a ring that
|
||||||
|
records everything alongside it -- `logcat` and a terminal see exactly
|
||||||
|
what they saw before:
|
||||||
|
|
||||||
|
client_core::log_ring::install_process_logger(
|
||||||
|
Box::new(android_logger::AndroidLogger::new(config)),
|
||||||
|
LevelFilter::Debug,
|
||||||
|
)?;
|
||||||
|
let ring = client_core::log_ring::process_ring(); // 2000 lines / 256 KiB
|
||||||
|
ring.to_text(); // for a report
|
||||||
|
ring.summary(); // "1801 lines held, 12 dropped, last 20:09:24"
|
||||||
|
|
||||||
|
// and, where the app has a server:
|
||||||
|
let upload = LogUpload::spawn(ring.clone(), transport, "iris-bench", Duration::from_secs(10));
|
||||||
|
upload.flush_now(); // what `Copy report` calls
|
||||||
|
upload.status().summary(); // "not tried yet" / "failing -- <why>" / "N lines sent"
|
||||||
|
|
||||||
|
`process_ring` is a deliberate process-global, unusually for this project:
|
||||||
|
`log` already has exactly one backend per process, and a ring passed around
|
||||||
|
as a parameter would be a second answer to "which lines exist". Dropping
|
||||||
|
the `LogUpload` handle stops and joins its thread. The wire format is one
|
||||||
|
new route on `ai-server`, `POST /client-log`; the reasoning and the
|
||||||
|
rejected alternatives are in docs/DECISIONS.md, 2026-09-07.
|
||||||
|
|
||||||
## 2026-09-07: `TextData` no longer bundles a font
|
## 2026-09-07: `TextData` no longer bundles a font
|
||||||
|
|
||||||
Iris's call: "remove the font for now; just match what compose does."
|
Iris's call: "remove the font for now; just match what compose does."
|
||||||
|
|||||||
+101
-5
@@ -89,6 +89,105 @@ Three things this says about the rig, since the rig is new:
|
|||||||
claimed of it -- asserted at the end of every draw, and the test
|
claimed of it -- asserted at the end of every draw, and the test
|
||||||
checks both directions.
|
checks both directions.
|
||||||
|
|
||||||
|
### Phone logging, 2026-09-07 (built and verified end to end)
|
||||||
|
|
||||||
|
**The problem**: Iris tests these builds on a phone with no `adb`, and
|
||||||
|
Android forbids one app reading another's `logcat`, so a `log::info!` in
|
||||||
|
the iris app could not reach her at all. What she asked for was Dev
|
||||||
|
Updater, which she already reads.
|
||||||
|
|
||||||
|
**The route, in one line**: the app keeps its own bounded log ring, posts
|
||||||
|
it to `ai-server`, and `ai-server` re-emits each line into its own
|
||||||
|
`tracing` output -- which Dev Updater *already* shows as that component's
|
||||||
|
**runtime log**, because it runs `ai-server` as a `Managed` service and
|
||||||
|
that service's script redirects stdout to
|
||||||
|
`$XDG_DATA_HOME/dev-updater/services/<key>-<component>/<...>.log` and
|
||||||
|
reports the path. **Nothing in dev-updater changed.** The alternatives and
|
||||||
|
what each would have cost are in docs/DECISIONS.md.
|
||||||
|
|
||||||
|
What exists now:
|
||||||
|
|
||||||
|
- `client_core::log_ring` -- `LogRing` (2000 lines / 256 KiB, whichever
|
||||||
|
bites first, with `dropped` reported rather than inferred), `RingLogger`
|
||||||
|
(a `log::Log` backend that records *and* forwards to the platform's own
|
||||||
|
logger), and `install_process_logger`. Reading does not consume, so the
|
||||||
|
report and the uploader are two readers of one ring.
|
||||||
|
- `client_core::log_upload` -- `LogUploader::flush_once` (batched at 500
|
||||||
|
lines, retried from the same cursor on failure) and `LogUpload::spawn`
|
||||||
|
(a thread flushing every 10s, stopped by dropping the handle).
|
||||||
|
**Nothing in it calls `log!`**: those lines would land in the ring it is
|
||||||
|
draining.
|
||||||
|
- `POST /client-log` on `ai-server` -- `{source, lines:[{seq, at, level,
|
||||||
|
target, message}]}`, re-emitted at the client's own level under the
|
||||||
|
target **`ai_server::client_log`**. Under the crate's path deliberately:
|
||||||
|
a bare `client_log` target is dropped by `RUST_LOG=ai_server=debug`, the
|
||||||
|
filter AGENTS.md tells people to run with, so every line a phone sent
|
||||||
|
vanished with nothing saying so. Found by running it.
|
||||||
|
- `iris/android-app/src/app_log.rs` -- the platform half only:
|
||||||
|
`android_logger` as the logger to forward to, and the destination baked
|
||||||
|
in by `build.rs` from `AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` plus the pinned
|
||||||
|
CA (all three or none). `Copy report` appends the ring to the clipboard
|
||||||
|
text and flushes the uploader first; `Diagnostics` gains two lines --
|
||||||
|
how many lines are held and when the last arrived, and what the uploader
|
||||||
|
last did.
|
||||||
|
|
||||||
|
**How to use it.** Build the APK with the destination in the environment,
|
||||||
|
on the machine `ai-server` runs on:
|
||||||
|
|
||||||
|
AI_APP_LOG_HOST=10.66.0.1 AI_APP_LOG_PORT=8443 \
|
||||||
|
AI_APP_LOG_TOKEN=<a token that server accepts> \
|
||||||
|
./build-apk.sh release
|
||||||
|
|
||||||
|
Then read it on the phone: Dev Updater -> the ai-app project -> the
|
||||||
|
**server** component's log button -> the **Runtime** tab. The app's lines
|
||||||
|
are the ones tagged `ai_server::client_log`, each carrying `[<source>
|
||||||
|
<the app's own clock> #<seq>]` before the target and message. A build with
|
||||||
|
none of those variables set still keeps its ring and still puts it on the
|
||||||
|
clipboard from `Copy report`; the Diagnostics pane says so in as many
|
||||||
|
words.
|
||||||
|
|
||||||
|
**Verified 2026-09-07, all four hops.** ai-server on 127.0.0.1:8455 with a
|
||||||
|
scratch config; the bench APK built `--abi x86_64` with
|
||||||
|
`AI_APP_LOG_HOST=10.0.2.2 AI_APP_LOG_PORT=8455 AI_APP_LOG_TOKEN=...`;
|
||||||
|
installed and launched on this checkout's emulator. Twenty seconds later
|
||||||
|
the server's log held
|
||||||
|
|
||||||
|
INFO ai_server::client_log: [iris-bench 20:17:02.284 #0]
|
||||||
|
iris::android::view: iris: new_peer content_scale=2.625
|
||||||
|
|
||||||
|
and a scratch `dev-updater` (port 8492, `XDG_DATA_HOME=/tmp/du-test/data`)
|
||||||
|
with this checkout registered served exactly those lines back from
|
||||||
|
`GET /apps/ai-app-2/components/server/logs?kind=runtime` -- which is the
|
||||||
|
JSON the phone's Runtime tab renders.
|
||||||
|
|
||||||
|
**Two rig traps this cost an hour to find, both in `build-apk.sh`, both
|
||||||
|
still there.** Written down rather than fixed because fixing them belongs
|
||||||
|
with whoever next touches that script:
|
||||||
|
|
||||||
|
1. **Gradle's merged-native-libs cache survives `rm -rf jniLibs`.** The
|
||||||
|
script removes `app/src/main/jniLibs` before each build (its own
|
||||||
|
comment says why), but Gradle's `mergeReleaseNativeLibs` is *up to
|
||||||
|
date* against its cached inputs, so a build that switches ABI packages
|
||||||
|
the previous ABI. A `--abi x86_64` release APK contained
|
||||||
|
`lib/arm64-v8a/libmain.so`, installed fine, and aborted at startup with
|
||||||
|
`Could not get adapter!: NotFound { active_backends: VULKAN }` under
|
||||||
|
`libndk_translation` -- which reads exactly like the phone's own Vulkan
|
||||||
|
problem and is nothing of the kind. `rm -rf app/build/intermediates`
|
||||||
|
before the build is the workaround; check with
|
||||||
|
`python3 -c "import zipfile; print([i.filename for i in
|
||||||
|
zipfile.ZipFile('...apk').infolist() if i.filename.endswith('.so')])"`.
|
||||||
|
2. **The debug bench APK is 648 MB and will not install**
|
||||||
|
(`INSTALL_PARSE_FAILED_NOT_APK`): the debug `libmain.so` is 325 MB.
|
||||||
|
Use `release` on the emulator for this app, notwithstanding the general
|
||||||
|
rule that the emulator stays on debug -- there is nothing to measure
|
||||||
|
here, and the debug build cannot be installed at all.
|
||||||
|
|
||||||
|
Also: the bench APK's package is `dev.iris.android.demo.bench`, not
|
||||||
|
`dev.iris.android.demo`. An older non-bench build left installed answers
|
||||||
|
to the second name, runs, looks right, and reports whatever *it* was built
|
||||||
|
with -- which is how "log upload: this build has no server configured"
|
||||||
|
came from a build that had one.
|
||||||
|
|
||||||
### APK size (2026-09-07)
|
### APK size (2026-09-07)
|
||||||
|
|
||||||
Iris's question: the iris bench APK is about double the Compose bench APK
|
Iris's question: the iris bench APK is about double the Compose bench APK
|
||||||
@@ -366,11 +465,8 @@ closes it.
|
|||||||
drawn through the header in the other (docs/IRIS_TODO.md, 2026-09-07).
|
drawn through the header in the other (docs/IRIS_TODO.md, 2026-09-07).
|
||||||
Done 2026-09-07, e922b73 + d507ae4; the root causes and the test names
|
Done 2026-09-07, e922b73 + d507ae4; the root causes and the test names
|
||||||
are in that IRIS_TODO entry, and the short version is below.
|
are in that IRIS_TODO entry, and the short version is below.
|
||||||
- [ ] Phone logging through Dev Updater (Iris has no logcat; see
|
- [x] Phone logging through Dev Updater -- **done 2026-09-07**, see
|
||||||
docs/TODO.md and the memory note): research how Dev Updater shows an
|
"Phone logging" above and docs/DECISIONS.md's entry of that date.
|
||||||
app's runtime log, design the smallest route (the app keeps its own
|
|
||||||
recent log; a debug button copies it; Dev Updater reads it), write
|
|
||||||
the decision in docs/DECISIONS.md, build it.
|
|
||||||
- [x] APK size: release profile tuned (`42af780`), -35% APK, -40% .so;
|
- [x] APK size: release profile tuned (`42af780`), -35% APK, -40% .so;
|
||||||
see "APK size (2026-09-07)". **Iris's verdict, 2026-09-07: "remove the
|
see "APK size (2026-09-07)". **Iris's verdict, 2026-09-07: "remove the
|
||||||
font for now; just match what compose does."** Done same day -- the six
|
font for now; just match what compose does."** Done same day -- the six
|
||||||
|
|||||||
Reference in new issue
Block a user