Changelog

All versions and downloads Jump to Added | Changed | Fixed | Security

Unreleased [Not released yet]

Part 3 of 11. Previous part

Changed (61)

  • An io_uring worker lends 28 receive buffers at start, not 32. When they run out, it lends a quarter more, at least 16. After the start proof, the server gives free malloc pages back to the system (malloc_trim, glibc only). The native-rank wisp app got 1000 keep-alive requests on /. Its VmRSS went from 4344 kB to 4288 kB (median of 20, 2 workers on CPUs 1-2, WSL 6.8). 256 keep-alive connections of 50 requests each had 0 errors. They had 201 receive rearms, from 193 (a counter in a test build, 2 runs). The work Ir per request does not change: 2183.0 on / and 2389.0 on /json (bench/native-rank/ir.sh, 3 runs, the build of commit 93335e0e6).
  • wisp dev compiles the app crate with -C opt-level=0 -C codegen-units=16 in every build that it runs (the first build and each save) when the profile has opt-level 0 or 1 (the scaffold has 1). A higher opt-level stays as the app sets it. The dependencies keep the profile, and wisp build and a cargo build are not changed. A save of a Rust edit shows in about 0.6 s, from about 1.5 s with the scaffold profile (opt-level = 1). Method: an app from wisp new on Windows (4 CPUs, MSVC), 6 saves that change a function body. The "Rebuilt in" line of wisp dev shows 0.5 s after and 1.5 s before (a poll of / every 10 ms until the new text shows: median 0.6 s after, 1.5 s before). One rustc call takes a median of 303 ms after and 1213 ms before, and the link takes 90 ms and 120 ms of that. The median time of a request to / in the dev server goes from 0.015 ms to 0.027 ms. Before, the app crate used the opt-level of the profile.
  • On the epoll and io_uring drivers, a page body or an owned body (Body::Bytes) of 16 KiB or more goes to the socket after the head with no copy into the write buffer. Only the last response of a read does this. A response before another one in the same read is copied as before. If the socket takes only part of the head on the epoll, the body is copied behind it.
  • The release workflow fails while the elan pin (ELAN_COMMIT, ELAN_SHA256) is not set. Before, it skipped the proofs step. The CI workflow still skips it. The cargo-deny check in the release and scheduled workflows matches the version line exactly. Before, 0.20.2 also matched 0.20.20.
  • The wasm build for Deno and Bun keeps error, redirect and WebSocket answers out of line, as the request-only build did. Deno compiles each wasm function on its first call. A table in memory skips its store check at start, so its code waits for its first use. The first GET / of the bench app compiled 121355 body bytes before, and 108863 after (local Deno 2.9.7 trace, --trace-wasm-compilation-times). Cold start, user instructions (Paris b103, base 7733f3e04): Deno raw 143.00M -> 140.19M (-2.0%), Deno.serve 155.94M -> 153.21M (-1.8%), workerd 116.80M -> 114.83M (-1.7%). Bun and every warm request are flat. Native builds are the same.
  • The type of plain text is text/plain;charset=UTF-8 on every target (native, edge, static), not text/plain; charset=utf-8. This covers Response::text, the built-in error and health answers, robots.txt and .txt files. At load, the bridge checks if new Response(string) has this type (Deno and Node do, Bun does not). Where it does, the bridge sends a plain text 200 with no headers object.
  • cx::decode (query and path parameters) checks UTF-8 without the lossy walk and keeps its decoded buffer, with no second copy. Deno raw /params/42?q=hello%20world&x=1 28.28k -> 27.94k, Deno.serve 40.47k -> 40.14k user instructions per request (Paris b54, 11 reps, disjoint, Wisp a76824924 on b45c83338).
  • Edge builds: Cx::set_cookie_with keeps one body for all value types, not one per type. The gate in the worktree measured tests/edge-full at 804978 bytes on 08b1505ea and 800353 bytes on 30cb4ae0f (-4625). tests/edge-size changed from 376120 to 376233 bytes (+113).
  • A Password in a job or in put_json is stored as its hash. A plain one is hashed when the job is pushed or stored.
  • Rows that Sql::all/first, Kv::get_json and a job read are not checked against #[validate].
  • Edge builds: an app without Response::websocket links no WebSocket handshake code. The wasm of such an app is smaller. The gate in the main checkout measured tests/edge-size at 376385 bytes on 61663dda7 and 370402 bytes on 57403acb5 (-5983). tests/edge-full, an app that uses WebSockets, changed from 801471 to 801498 bytes (+27).
  • Edge builds: an instance keeps the Cx of an answered request and gives it, clean, to the next request. Before, each request made a new Cx on the heap and freed it. A request that starts while the kept Cx is in use gets a new one. The native server does not change. On Deno.serve / (Paris, Wisp 20e72be83 with the edgefull2 patch, median of 11, A/A runs equal), it runs 19.35k user instructions per request, not 19.62k. Raw Deno does not change. The edge-size wasm grows by 3051 bytes.
  • Edge builds: through the fetch handler (serve), the bridge does not read the body of a GET or HEAD request. The app gets these requests with no body. On Deno 2.9.7 and Bun, request.body of a GET with content-length: 5 or a chunked body is null (raw-socket run). So the change does not change what an app gets on these two hosts. The native server still gives a GET body to cx.body(). On Deno.serve / (Paris, Wisp 8cbd05281 on 7e9c4e1d1, median of 11), it runs 290 fewer instructions per request (20.78k to 20.49k). Raw sockets do not change.
  • Edge builds: the header cache of a lazy request is behind one pointer. A request that reads no header makes no cache. A request that reads one uses the cleared cache of the last request.
  • Edge builds: setup runs once per instance, in prepare. It does not run for each request. The setup change alone is inside the spread of the runs. The two changes above together, on Deno.serve / (Paris, Wisp 4f7230291 on 17eff84f0, median of 11), run 340 fewer instructions per request (21.12k to 20.78k). Raw sockets do not change. Native routes do not change.
  • The edge build answers a request to a sync +server.rs arm (App::handle_now) in the call of the host, with no task, as the epoll driver of the native server does. An error page, an arm that waits, a stream, a WebSocket and changes not yet saved go on through the task. An app with a before hook has no such arm. Raw sockets of Node, Bun and Deno answer such an arm the same way. The wasm build leaves out the async shim of such an arm. On / (Paris, Wisp 01c3222cf on 2280dd0a5, median of 11), Deno.serve runs 890 fewer instructions per request (20.51k to 19.62k) and raw sockets run 300 fewer (21.34k to 21.04k).
  • Pure routes on the edge. A sync +server.rs get that takes no Cx and no input, on a path with no param, can be pure (App::pure). It is pure when no hook, guard, CACHE, COMPRESS, CORS or TIMEOUT is on it. The bridges that answer through the host's fetch handler are Deno.serve, Bun.serve, Workers, Netlify and Vercel Edge. A GET of such a path with no query can send a 200 with only a static content-type. Then these bridges keep the path and host. Later GETs of them run the handler in one call, with no request bytes and no Cx. Any other result goes the whole way with that result, so the handler runs once. Other results are another status, a header, an error, a stream or changes not yet saved. An app with no pure route has no App::pure. A bridge that finds no pure call at startup does not use it. Raw sockets of Node, Bun and Deno do not use it. User instructions per request on / (Paris b86, base 7733f3e04): Deno.serve 19.37k -> 15.91k, Bun.serve 17.54k -> 15.77k, workerd 232.13k -> 205.57k.
  • Node's node:http handler (WISP_NODE_HTTP=1, Vercel's Node function, Firebase) uses pure routes too. The first GET of a path goes as a lazy request, as on Deno.serve. That request can make the path pure. Later GETs of a pure path run the handler in one call. Only a GET with no query, no body header, one host that is not empty and each header name once uses this path. Each value must be Latin-1 with no control byte but a tab. All other requests go whole, as before. A whole request has content-length: 0, and so has the lazy one.
  • Edge builds: a task poll uses one kept waker. It does not clone and drop the waker's Arc for each poll. A task that keeps a waker still wakes, and the next poll gets a new waker. A poll inside a poll gets its own waker. On Deno.serve / (Paris, median of 21) the change is inside the spread of the runs: no gain is measured. Native routes do not change.
  • Edge: the parser writes each request into a Cx on the heap in place. The request path does not copy the Cx from frame to frame. On Deno.serve / (Paris, Wisp d9d973b0 on 0e25654f, median of 11) it runs 930 fewer instructions per request (22.25k to 21.32k). Deno raw sockets and native routes do not change.
  • Edge: a bodiless request through serve of the edge bridge (Deno.serve) goes into its Cx with no parse when its head has the shape that the bridge writes: a request line and one host line. Wisp checks the same things as the parser: method, path bytes, host bytes and the 16 KiB head limit. Any other head goes to the parser as before. On Deno.serve / (Paris, Wisp 6d3a874c on d22cadb0, median of 11) it runs 290 fewer instructions per request (21.45k to 21.16k). Raw sockets and native routes do not change.
  • Edge builds: when a reply has only static headers at the same addresses as the last reply, send uses the last head and does not compare its bytes. The sent bytes do not change. On Deno.serve / (Paris, Wisp fdbb0af9 on 27803028, median of 11) it runs 550 fewer instructions per request (22.94k to 22.39k). Deno raw sockets do not change.
  • Edge build: a request uses the request buffers of an answered request. It does not allocate them again. On Deno.serve / (Paris, Wisp 7a9d0991, median of 11) it runs 160 fewer instructions per request (23.12k to 22.96k). Wisp raw does not change.
  • Edge builds: the request parser reads a header value eight bytes at a time and skips the scan of 16 bytes. Wasm has no vector compare, so that scan was a loop over each byte. A failed scan of 16 bytes also ran again after each next word. On Deno raw sockets / (Paris, Wisp c4597060 on 77624185, median of 11) it runs 170 fewer instructions per request (20.70k to 20.53k). Deno.serve and native routes do not change.
  • Edge builds: a reply with the same head as the reply before it skips the head write and the search in the sent heads. It sends the same head number. A byte-by-byte check of status, flag and headers decides. On Deno.serve / (Paris, Wisp 3038cd91, median of 11) it runs 390 fewer instructions per request (23.51k to 23.12k). With / and /json mixed (Paris, two oha runs at once, same code), the change (-0.21k) is below the run spread (3.9%): not resolved.
  • The edge wasm keeps wisp.js and live.js gzipped and unpacks each once, on the first request that sends it. The wasm is smaller, and the served bytes stay the same.
  • Deno raw sockets: a socket write is a plain promise chain, with no async function and no catch promise for each write.
  • The gate has a proofs step. It runs lake build in proofs/, which holds Lean 4 proofs of premises of the request path. With no lake, the step prints proofs: SKIPPED (no lake).
  • Response::with_header takes a value of impl Into<Cow<'static, str>>. A &'static str value is not copied, so .with_header("cache-control", "no-store") allocates nothing. Response::headers is Vec<(Cow<'static, str>, Cow<'static, str>)>. A borrowed &str that is not 'static needs .to_string().
  • A table keeps its rows in one Vec in the order of their adds, not in a BTreeMap. An index of ids in sorted chunks of at most 256 finds each row. An add with a new highest id is a push. An add at another id (a random-id table) moves at most one chunk of the index, and never a row. A load sorts the ids once. The edge wasm is smaller.
  • wisp build --target deno: a read on a connection adds one to a counter. The idle check reads the clock, not each read. The idle time starts at the first check that sees the new read or write, not at the read. So a quiet connection can stay open up to one check interval (a quarter of the idle limit) longer than before.
  • The edge answer keeps the check of a 'static header pair, as the native server does. A thread checks such a pair again only when it is not one of its last 4 pairs.
  • wisp openapi: a named action (?/send) is no longer a path key with a query, which OpenAPI 3.1 does not allow. It is an operation in the x-wisp-action-send field of the page's path item. fn default stays the post of the page.
  • The TFB bench runs wrk on 4 CPUs, because 2 CPUs limit it. A cell where the median busy share of the busiest client CPU is 95% or more gets no rank. Actix Web sets its workers to the CPUs it can use, because actix-server 2.2.0 ignores taskset. The numbers on the site are from a WSL2 VM on a Ryzen 7 7800X3D.
  • Benchmark validity is per cell. bench/quiet/summarize.mjs writes valid_cells, valid_fraction and publishable in place of the run-level valid. A run is publishable when the gates pass, no cell is unclassified and at least 90 % of the cells are valid (PUBLISH_FLOOR). Reports show only valid cells and show each other cell as "noisy, not shown". Before, one noisy cell blocked the whole run.
  • Release builds send a smaller browser runtime. Its minify step drops each ; before } and joins let statements (let a=1,b=2). The meaning of the code stays the same. Gzip bytes (gate js step): wisp.js 10_250 to 10_186, live.js 12_539 to 12_457, extra.js 6_291 to 6_242.
  • The offline queue of data-wisp-queue moved from wisp.js to /_app/c/queue.js. wisp.js imports it while online on a page with such a form, and when a page starts with posts that wait. Gzip bytes (gate js step): wisp.js 10_180 to 10_178, queue.js 386. After a failed import, the next try asks for queue.js#1, #2 and so on, because a browser may keep the failure for that URL. A static export writes queue.js beside wisp.js. The runtime serves queue.js to every app, also an app with scripts = false.
  • tweened, spring and crossfade moved from extra.js to /_app/c/motion.js, and the browser code of t('key') moved from live.js to /_app/c/i18n.js. Only a module that names one of the three loads motion.js. Only an app with src/locales loads i18n.js. Gzip bytes (gate js step): live.js 12_457 to 12_324, extra.js 6_242 to 5_305, wisp.js 10_186 to 10_180.
  • version of import { version } from 'wisp' is now the id of the build (the same as <meta name="wisp-build"> and /_app/version.json, an empty string in wisp dev). Before, it was the [package] version.
  • The generated Dockerfile builds with --locked. wisp deploy init docker stops with a message when Cargo.lock is missing. A missing wasm target error says to run rustup target add in the app folder, for the toolchain in rust-toolchain.toml.
  • Edge builds (Node, Bun, Deno, Cloudflare): the JSON writer writes a short string (8 bytes or fewer, nothing to escape) one byte at a time, with no memory.copy. /json-big uses 3.0% fewer user instructions a request on Deno and 5.6% fewer on Node (median of 7 reps, VPS). The wasm grows 570 bytes. Native code does not change.
  • Apps that use wisp::Table get a smaller wasm: the edge build has no store poll and no test::fresh code (14,523 B less in a test app with one saved table, 454,715 to 440,192 B, measured on one PC).
  • Apps that use wisp::Table with more than one row type get a smaller wasm: the check for two saved tables with one name is one shared function for all row types (3,525 B less in the test app, measured on one PC).
  • A release edge wasm build has no /_wisp/* dev endpoint. With WISP_DEV=on, the app's router answers those paths (its own 404 page when no route matches), not a plain 404. The wasm is smaller.
  • beforeNavigate: nav.to is now URL | null (null for leave).
  • A template hole that is one format!(..) call (${format!("Item <{i}> & co")}) writes into the page. It makes no String for the value. When the format string holds only text and captures ({name}, {name:spec}), the build escapes the text between the captures once. A {name} capture writes as ${name} does, so a number takes the integer path. Any other arguments (format!("{}", x)) format into the escaper. The text on the page is the same as before. When a Display impl returns Err, the page keeps the text written so far. Before, format! panicked.
  • The gate checks more. The ste step reads the comments, docs and messages of tests/, examples/, bench/ and editors/ too, with a baseline per file that can only fall. The audit step fails on two ledger lines for one file. The stamps step rejects a <Measured at="newest"> block that names a group such as rank and not one suite of bench/suites.tsv, because the hash binds only exact suite names. The test fast.rs fails in CI (CI is set) when node or the wasm32-unknown-unknown target is missing, and does not skip. The release workflow publishes the VS Code extension only after the binaries, the crates and the release job passed. The GitHub mirror of the workflows sets permissions: contents: read at the top.
  • The last line of the reference in the AGENTS.md of an app has no semicolon now: <!-- End of the Wisp reference. Notes for this app go below. wisp update-docs keeps them. -->. wisp update-docs looks for this exact line. An app made before this change has the old line, so wisp update-docs says that it finds no end line. Move your notes to another file, delete AGENTS.md, and run wisp update-docs again.
  • The site step of the gate counts a changelog version page without its release body (the <article> element) under the HTML budget. The body has its own ceilings, 29,800 bytes and 11,450 bytes gzipped. They come from the page of 1.0.0-pre.3. The pages of changelog/unreleased/ have the same ceilings. The gate has no exemption for them.
  • cargo run -q -p wisp-gate site --dist <dir> checks a site that is already built in <dir> against the speed budgets. It builds nothing and needs no docs checkout. Without --dist, site builds the reference site as before.
  • A WebSocket upgrade on WISP_LISTEN=unix: logs wisp: WebSocket on "<path>" refused: WebSockets need a TCP listener, not WISP_LISTEN=unix:. The server still writes the 101 and closes the connection. Before, it closed with no log.
  • The onion page gives the reason for no Secure cookie on a plain-http onion: Tor Browser treats a .onion address as a potentially trustworthy origin, and Wisp did not check which other clients refuse a Secure cookie there. Before, the page said that the browser may refuse it.

Fixed (464)

  • wisp dev --target <host> --port 0 is an error: --port 0 does not go with --target. Before, Wisp passed PORT=0 to the host and waited 30 s for a host on port 0.
  • The error dialog of wisp dev names the file and line of a template error when the path starts with a Windows drive (C:\app\src\routes\+page.wisp:4:1). Before, it named no place.
  • wisp dev counts a save as a change after a build when a Rust file keeps its size and its time stamp but not its text. Before, it kept the code of the file from before the build.
  • The Pages warning about static/ entries that reach the worker uses the singular for a count of 1 (1 top-level name, 1 entry reaches the worker, which serves it). Before, it said 1 top-level names and 1 entries reach.
  • const EARLY_HINTS_H1 in src/hooks.rs or in a +server.rs fails the build, as EARLY_HINTS does. Before, the build ignored it there.
  • The docs of Browser::eval said that a value that is not JSON gives Value::NULL. undefined gives Value::NULL, and an object (an element too) gives its own fields. A browser may fail to send some values (for example window). Then eval fails with the browser's error. For such a statement, use void (...) or return JSON (el.id).
  • On a CACHE or CACHE_PUBLIC route whose handler calls cx.cors, the kept answer holds each header that the handler set, also those before cx.cors. With cx.cors("*"), a request with any origin gets a hit. When access-control-allow-origin echoes an origin, the kept answer holds one origin at a time. A request with the same origin gets a hit, and alternating origins run the handler again. Before, a hit sent no access-control-allow-origin and no vary: origin.

Next part (4 of 11)

Source: CHANGELOG.md