01f721d8d1a718c7b1f192b3af3d110f7d84d340
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
01f721d8d1 |
Cut what a signed-in tab costs by two thirds
Two changes on the push path, both measured against a real Stalwart 0.16.20
with the container capped at 256 MiB and tabs added in steps of 200 until the
kernel killed it:
tabs held per tab of which native
before 1,665 133 KiB 81 KiB
pin upstream calls to STALWART_URL 3,400 58 KiB 8 KiB
+ raw push relay 4,979 37 KiB 10 KiB
Stalwart advertises absolute https URLs in every session, and the proxy
followed them -- so even with STALWART_URL naming a private plain-HTTP hop on
the same Docker network, every held push stream went out through TLS. That leg
is about 80 KiB of OpenSSL state per tab: native memory Node cannot see, which
is why neither the heap ceiling nor the stream buffer size ever moved the
number. absoluteUpstream() now keeps the path and query from the advertised
URL and the scheme, host and port from the configured one. A setup that must
reach Stalwart at an origin other than the one it was given sets
STALWART_FOLLOW_ADVERTISED_URLS=1.
With the transport out of the way, the fetch()-based relay was the next cost:
an undici Response, a web ReadableStream, a reader and Hono's stream bridge
held alive per tab, about 44 KiB of heap for a session that otherwise costs
4 KiB. relayPushRaw() pipes the upstream socket into the Node response and
tells the adapter the response is already sent. RAW_PUSH_RELAY=0 restores the
fetch path for comparison.
JMAP throughput is unchanged (2,383/s against 2,484/s at 50 users, inside
run-to-run noise); the relay does not touch that path. Verified that a push
stream through the raw relay delivers a StateChange while mail is written.
The install page's advice to set --max-old-space-size was measured in the same
runs and made no difference at all -- 3,400 tabs with it and without -- and
is withdrawn in the docs alongside this change.
|
||
|
|
5356e603fe |
Compress our own responses (#288)
* Compress our own responses The bundle went out uncompressed unless a proxy in front did the work: 933 KB on the wire where 311 KB does, on every first load. Both example proxy configs compress, but that only helps deployments that copied them, and the default should not depend on reading the examples. Hono's middleware, with the proxy routes held back. `/api/blob`, `/api/image`, `/api/ics` and `/api/upload` forward somebody else's bytes under a content-length copied from upstream, and issue #76 was a silent truncation caused by exactly that header disagreeing with its body. Re-encoding them would be safe in principle -- the length is dropped and the response goes out chunked -- but they carry attachments and images that are already compressed, so there is nothing to win and a scar to respect. `/api/events` is listed with them even though Hono already skips text/event-stream by content type, so that changing the push route's type cannot quietly start buffering the stream. `/api/health` is excluded for the opposite reason: at 47 bytes gzip made it 73. Hono's size threshold cannot catch that on its own, because it only applies when a response carries a content-length and `c.json()` does not set one. The other JSON routes stay compressed -- a JMAP response has just as unknown a length and can run to hundreds of kilobytes. Verified against the built image: assets come back gzipped with Vary set, 662 KB to 209 KB; /api/events still returns text/event-stream with no content-encoding and delivered a StateChange while mail was being written; health is 47 bytes either way. No user-visible strings, so no catalogue work. * Word the comment for either side compressing The app compresses its own responses as of the follow-on change, so a note saying the bundle ships uncompressed would be wrong as soon as that lands. nginx passes through what the upstream already encoded rather than re-encoding it -- verified single-encoded with both layers active -- so the directives are correct either way and the comment now says so without asserting which side does the work. * Test compression against a fixture, not the web build The compression tests asked for `/` and asserted a gzipped 200. That passes locally, where `web/dist` is lying around from an earlier build, and fails in CI, which runs `npm test` before `npm run build`: with no bundle the shell route serves the "web build not found" fallback, which is short, plain text and correctly uncompressed. The failure read as compression being broken when the tests were simply depending on a build step that had not run. They now build their own static root in a temp directory and point STATIC_DIR at it, in a separate file so the environment is set before the app module is imported. Checked by moving web/dist aside and running the suite the way CI does. |