mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-21 09:29:44 -06:00
Update documentation to cover the new WASM runtime.
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
An open, <a href="https://trailbase.io/reference/benchmarks/">blazingly fast</a>,
|
An open, <a href="https://trailbase.io/reference/benchmarks/">blazingly fast</a>,
|
||||||
single-executable Firebase alternative with type-safe REST & realtime APIs, built-in JS/ES6/TS
|
single-executable Firebase alternative with type-safe REST & realtime APIs, built-in JS/ES6/TS
|
||||||
runtime, SSR, auth and admin UI built on Rust, SQLite & V8.
|
runtime, SSR, auth and admin UI built on Rust, SQLite & WebAssembly.
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default defineConfig({
|
|||||||
"http://localhost:4000/**/*",
|
"http://localhost:4000/**/*",
|
||||||
// The link validator fails to validate the OpenAPI pages injected above.
|
// The link validator fails to validate the OpenAPI pages injected above.
|
||||||
`/${openApiBase}/**/*`,
|
`/${openApiBase}/**/*`,
|
||||||
|
"/blog/**/*",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ Chart.register(BarWithErrorBarsController, BarWithErrorBar, ChartDeferred);
|
|||||||
|
|
||||||
interface BarChartProps {
|
interface BarChartProps {
|
||||||
data: ChartData<"bar">;
|
data: ChartData<"bar">;
|
||||||
scales?: { [key: string]: ScaleOptions<"linear"> };
|
scales?: {
|
||||||
|
[key: string]: ScaleOptions<"linear"> | ScaleOptions<"logarithmic">;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BarChart(props: BarChartProps) {
|
export function BarChart(props: BarChartProps) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ if (image) {
|
|||||||
<p>
|
<p>
|
||||||
An open, blazingly fast, single-executable Firebase alternative with
|
An open, blazingly fast, single-executable Firebase alternative with
|
||||||
type-safe REST & realtime APIs, built-in JS/ES6/TS runtime, SSR, auth
|
type-safe REST & realtime APIs, built-in JS/ES6/TS runtime, SSR, auth
|
||||||
and admin UI built on Rust, SQLite & V8.
|
and admin UI built on Rust, SQLite & WebAssembly.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -139,11 +139,9 @@ const href = pathWithBase(Astro.props.locale || "/");
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{import.meta.env.DEV && (
|
<a class="site-title-link" href="/blog">
|
||||||
<a class="site-title-link" href="/blog">
|
Blog
|
||||||
Blog
|
</a>
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
63
docs/src/content/blog/_switching_to_a_wasm_runtime.tsx
Normal file
63
docs/src/content/blog/_switching_to_a_wasm_runtime.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { ChartData } from "chart.js/auto";
|
||||||
|
import { BarChart } from "@/components/BarChart.tsx";
|
||||||
|
|
||||||
|
// const green0 = "#008b6dff";
|
||||||
|
const green1 = "#29c299ff";
|
||||||
|
const blue = "#47a1cdff";
|
||||||
|
const purple0 = "#ba36c8ff";
|
||||||
|
const purple1 = "#c865d5ff";
|
||||||
|
const purple2 = "#db9be3ff";
|
||||||
|
const yellow = "#e6bb1eff";
|
||||||
|
|
||||||
|
export function RuntimeFib40Times() {
|
||||||
|
const data: ChartData<"bar"> = {
|
||||||
|
labels: ["100 runs fib(40) [less is faster]"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "V8",
|
||||||
|
data: [26.96],
|
||||||
|
backgroundColor: green1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "WASM Rust",
|
||||||
|
data: [7.14],
|
||||||
|
backgroundColor: blue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "WASM SpiderMonkey JS",
|
||||||
|
data: [29 * 60 + 43],
|
||||||
|
backgroundColor: purple0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "WASM SpiderMonkey JS + weval",
|
||||||
|
data: [18 * 60 + 47],
|
||||||
|
backgroundColor: purple1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "WASM custom QuickJS",
|
||||||
|
data: [11 * 60 + 36],
|
||||||
|
backgroundColor: purple2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "PocketBase (Goja JS)",
|
||||||
|
data: [16 * 60 + 12],
|
||||||
|
backgroundColor: yellow,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BarChart
|
||||||
|
data={data}
|
||||||
|
scales={{
|
||||||
|
y: {
|
||||||
|
type: "logarithmic",
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Time [s]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
docs/src/content/blog/_wasm_logo.svg
Normal file
8
docs/src/content/blog/_wasm_logo.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created by AtomCrusher for the English Wikipedia -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="612" height="612">
|
||||||
|
<!-- Block -->
|
||||||
|
<path d="m376 0c0 1.08 0 2.16 0 3.3 0 38.76-31.42 70.17-70.17 70.17-38.76 0-70.17-31.42-70.17-70.17l0 0c0-1.14 0-2.22 0-3.3L0 0l0 612 612 0 0-612z" fill="#654ff0"/>
|
||||||
|
<!-- Letters -->
|
||||||
|
<path d="m142.16 329.81 40.56 0 27.69 147.47 0.5 0 33.28-147.47 37.94 0 30.06 149.28 0.59 0 31.56-149.28 39.78 0-51.69 216.69-40.25 0-29.81-147.47-0.78 0-31.91 147.47-41 0zm287.69 0 63.94 0 63.5 216.69-41.84 0-13.81-48.22-72.84 0-10.66 48.22-40.75 0zm24.34 53.41-17.69 79.5 55.06 0-20.31-79.5z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 710 B |
296
docs/src/content/blog/switching_to_a_wasm_runtime.mdx
Normal file
296
docs/src/content/blog/switching_to_a_wasm_runtime.mdx
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
---
|
||||||
|
title: >
|
||||||
|
Switching to a WebAssembly Runtime
|
||||||
|
pubDate: 2025-09-09
|
||||||
|
intro: Going forward TrailBase will rely on a WebAssembly rather than a V8 JavaScript runtime.
|
||||||
|
tags: ["WASM"]
|
||||||
|
author: sebastian
|
||||||
|
image: ./_wasm_logo.svg
|
||||||
|
---
|
||||||
|
|
||||||
|
import { RuntimeFib40Times } from "./_switching_to_a_wasm_runtime.tsx";
|
||||||
|
|
||||||
|
TrailBase has been embedding a V8 JavaScript runtime for the last 10 months
|
||||||
|
allowing users to implement custom HTTP and job handlers.
|
||||||
|
During this time we've experienced several issues and limitations - we're
|
||||||
|
therefore excited to announce that TrailBase is adopting
|
||||||
|
[wasmtime](https://github.com/bytecodealliance/wasmtime) as a WebAssembly (WASM)
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
This is - and likely will remain - the biggest user-facing change to TrailBase
|
||||||
|
🙏.
|
||||||
|
To ease migration, the plan for releases v0.17 and v0.18 is to be transitional,
|
||||||
|
i.e. support both runtimes.
|
||||||
|
First, v0.17 makes the new WASM runtime available allowing us to collect early
|
||||||
|
feedback and address issues.
|
||||||
|
We don't expect major changes to the guest APIs. We put a lot of effort into
|
||||||
|
making the first release usable and all examples have already been migrated.
|
||||||
|
If everything goes to plan 🤞, v0.18 will then mark V8 for deprecation to
|
||||||
|
remove it in subsequent releases.
|
||||||
|
|
||||||
|
In the following we're going to touch a bit more about the rational, the
|
||||||
|
opportunities and what to watch out for.
|
||||||
|
|
||||||
|
## Rational
|
||||||
|
|
||||||
|
Before getting into the benefits of the new runtime, let's quickly touch on some
|
||||||
|
of the issues we had.
|
||||||
|
|
||||||
|
The V8 JavaScript engine is a amazing piece of engineering.
|
||||||
|
However, it was never designed as an embeddable or backend-first solution.
|
||||||
|
It's primary target remains the Chrome browser.
|
||||||
|
Third-party vendors like Node.js and Deno have taken it upon themselves to
|
||||||
|
extend V8, with APIs for accessing the file-system, sockets, etc.
|
||||||
|
These extensions themselves are extensive and have sprawling dependencies.
|
||||||
|
They're also primarily designed to serve their own ecosystem rather than be
|
||||||
|
embedded elsewhere.
|
||||||
|
For reference, ~70% of the `trail` binary are the JavaScript runtime while only
|
||||||
|
linking a subset of Node.js APIs.
|
||||||
|
Despite well written, this is a huge chunk of unsafe code with a heightened
|
||||||
|
level of scrutiny on it due to its pivotal role in browsers.
|
||||||
|
|
||||||
|
In practice, the current JS runtime isn't serving anyone particularly well:
|
||||||
|
|
||||||
|
* it's not Node.js compatible,
|
||||||
|
* it inflates binary size and the security surface,
|
||||||
|
* newer deno versions bundle a stale SQLite causing linker issues, and
|
||||||
|
* prevents us from building "truly" static binaries with MUSL[^1].
|
||||||
|
|
||||||
|
Comparably, the new WASM runtime is a lot simpler, safer and yet
|
||||||
|
high-performant.
|
||||||
|
It also supports [WASI](https://wasi.dev/)[^2], which greatly eases embedding and allows us to
|
||||||
|
support guests in multiple languages.
|
||||||
|
|
||||||
|
## Opportunities
|
||||||
|
|
||||||
|
Finally, let's talk about some of the immediate and future benefits we can expect...
|
||||||
|
|
||||||
|
### Rigorous State Isolation
|
||||||
|
|
||||||
|
V8's isolates and JIT are expensive, thus they're typically re-used across
|
||||||
|
requests opening up the gates for accidental state sharing.
|
||||||
|
There are specialized JIT-free "edge" runtimes like
|
||||||
|
[LLRT](https://github.com/awslabs/llrt) to specifically solve this issue at the
|
||||||
|
expense of performance/throughput.
|
||||||
|
Wasmtime, on the other hand, makes it cheap and easy to spawn fully isolated
|
||||||
|
instances per request[^4].
|
||||||
|
We expect this provide immediate safety benefits for users.
|
||||||
|
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
This one is a bit more mixed, however combined with an efficient guest environment
|
||||||
|
(e.g. Rust, C++, ...), wasmtime can outperform V8 by a factor of almost 4.
|
||||||
|
On the flip side, JS guests will be slower. More on that regression below.
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="h-[360px] w-[70%]">
|
||||||
|
<RuntimeFib40Times client:only="solid-js" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Flexible Guest Language Choice
|
||||||
|
|
||||||
|
Many languages support compilation to WASM. This gives users more freedom in
|
||||||
|
choosing and customizing their server-side environment.
|
||||||
|
For now, TrailBase supports JS/TS and Rust out-of-the-box.
|
||||||
|
We have plans to support
|
||||||
|
[more](https://github.com/bytecodealliance/wasmtime/blob/main/README.md#language-support)
|
||||||
|
in the future.
|
||||||
|
Independently and maybe more importantly, WASI makes it straight-forward to
|
||||||
|
support custom guests.
|
||||||
|
|
||||||
|
Moreover, different endpoints can be implemented in different WASM components
|
||||||
|
and thus different languages, allowing you to optimize performance as you go.
|
||||||
|
For example, an expensive, high-QPS endpoint could be rewritten in Rust, which
|
||||||
|
could yield 10x-100x performance gains.
|
||||||
|
|
||||||
|
### Better I/O Sandboxing
|
||||||
|
|
||||||
|
Previously, isolates were given untethered I/O access, which together with a
|
||||||
|
very dynamic guest language can pose a security risk.
|
||||||
|
I/O is now limited to read-only file access for an explicitly provided sandbox
|
||||||
|
root (`--runtime-root-fs`).
|
||||||
|
We're planning ot extend I/O capabilities over time on a per-need basis.
|
||||||
|
If you're missing anything, let us know.
|
||||||
|
|
||||||
|
The new integration also fixes timers - such as `setTimeout` and `setInterval`, which
|
||||||
|
were unreliable in our previous V8 integration.
|
||||||
|
|
||||||
|
### Less Code
|
||||||
|
|
||||||
|
As mentioned before, switching off V8 removes a lot of high-scrunity code, cuts
|
||||||
|
binary size roughly in half and lets us build truly static binaries with MUSL.
|
||||||
|
|
||||||
|
### Rethinking Composition & Licensing Model
|
||||||
|
|
||||||
|
The increased flexibility and performance provided by the new WASM runtime
|
||||||
|
opens up a path to making this a singular entry-point for extending TrailBase
|
||||||
|
(including SQLite extensions) as opposed to framework use-caess.
|
||||||
|
In turn, this may allow us to adopt a more popular copyleft license w/o
|
||||||
|
inflicting obligations on your business-logic.
|
||||||
|
|
||||||
|
|
||||||
|
## Regressions
|
||||||
|
|
||||||
|
While WASM can be handily faster than V8 - as seen above - JavaScript in
|
||||||
|
particular loads and runs significantly slower when compared to a highly
|
||||||
|
specialized and optimized runtime like V8.
|
||||||
|
What may feel like a step backward from a JS-centric point of view, may also
|
||||||
|
provide opportunities to optimize individual endpoints in different languages
|
||||||
|
based on specific needs.
|
||||||
|
|
||||||
|
In practice, JS is probably one of the least-efficient compile-to-WASM
|
||||||
|
languages. Instead of emitting immediate WebAssembly, current build flows
|
||||||
|
bundle an interpreter like SpiderMonkey to work around JS' dynamic nature, i.e.
|
||||||
|
`eval('/* ... */')`[^3].
|
||||||
|
|
||||||
|
In practice, using [JCO with SpiderMonkey and weval](https://github.com/bytecodealliance/jco.git)
|
||||||
|
is about as fast as Goja - PocketBase's JS interpreter - but about 40x slower
|
||||||
|
than V8.
|
||||||
|
On the other hand, Rust compiled to WASM is almost 4x faster than V8 with more
|
||||||
|
predictable latency and a lower resource footprint.
|
||||||
|
A benefit of the new runtime integration is that different endpoints can be
|
||||||
|
implemented in different languages providing extra flexibility and potential to
|
||||||
|
optimize.
|
||||||
|
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
The first difference you'll encounter is the need for a build-step: JS/TS -> WASM[^5].
|
||||||
|
For now, we recommend to copy the template in `examples/wasm-guest-ts` or
|
||||||
|
`examples/wasm-guest-ts`, dependning on whether you prefer TypeScript or
|
||||||
|
JavaScript respectively.
|
||||||
|
You can then simply run `pnpm install && pnpm build` to build the WASM component.
|
||||||
|
For TrailBase to pick up you `*.wasm` component, they need to be placed in
|
||||||
|
`<traildepot>/wasm/`.
|
||||||
|
|
||||||
|
The second big difference you'll notice right away is that the APIs for
|
||||||
|
registering endpoints had to change to work in the context of the short-lived
|
||||||
|
and isolated runtime instances.
|
||||||
|
Previously we were relying on global state for routing.
|
||||||
|
To avoid re-initialization on every request and for consistency with guest
|
||||||
|
languages that do not support eager initialization of globals, we're now
|
||||||
|
using module exports.
|
||||||
|
|
||||||
|
TypeScript endpoint before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
addRoute(
|
||||||
|
"GET",
|
||||||
|
"/test",
|
||||||
|
stringHandler(async (req: StringRequestType) => {
|
||||||
|
const uri: ParsedPath = parsePath(req.uri);
|
||||||
|
|
||||||
|
const table = uri.query.get("table");
|
||||||
|
if (table) {
|
||||||
|
const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []);
|
||||||
|
return `entries: ${rows[0][0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `test: ${req.uri}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and after:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineConfig({
|
||||||
|
httpHandlers: [
|
||||||
|
HttpHandler.get("/test", (req: Request) : string => {
|
||||||
|
const table = uri.getQueryParam("table");
|
||||||
|
if (table) {
|
||||||
|
const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []);
|
||||||
|
return `entries: ${rows[0][0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `test: ${req.url()}`;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively in Rust:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use trailbase_wasm::db::{query, Value};
|
||||||
|
use trailbase_wasm::http::{HttpError, HttpRoute, StatusCode, routing};
|
||||||
|
use trailbase_wasm::{Guest, export};
|
||||||
|
|
||||||
|
struct Endpoints;
|
||||||
|
|
||||||
|
impl Guest for Endpoints {
|
||||||
|
fn http_handlers() -> Vec<HttpRoute> {
|
||||||
|
return vec![
|
||||||
|
routing::get("/test", async |req| {
|
||||||
|
let Some(table) = req.query_param("table") else {
|
||||||
|
return Ok(format!("test: {:?}", req.url()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = query(format!("SELECT COUNT(*) FROM '{table}'"), [])
|
||||||
|
.await
|
||||||
|
.map_err(|err| HttpError::message(StatusCode::INTERNAL_SERVER_ERROR, err))?;
|
||||||
|
|
||||||
|
return Ok(format!("entries: {:?}", rows[0][0]));
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export!(Endpoints);
|
||||||
|
```
|
||||||
|
|
||||||
|
See `/examples/wasm-guest-(js|rust|ts)` for further examples while we continue
|
||||||
|
to improve the documentation.
|
||||||
|
|
||||||
|
## Next-Steps
|
||||||
|
|
||||||
|
First and foremost, we'd love to hear from you, make the transition as smooth
|
||||||
|
as possible and the WASM runtime best-in-class 🙏.
|
||||||
|
|
||||||
|
Once the new runtime integration has seen more mileage and unforeseen surprises
|
||||||
|
are worked out, we'd like to sunset V8 expediently.
|
||||||
|
This will provide immediate benefits in terms of portability, security,
|
||||||
|
build-times and binary sizes.
|
||||||
|
|
||||||
|
From that point on we plan to invest heavily into making the integration the
|
||||||
|
best we can.
|
||||||
|
With the previous V8 integration, given its rough edges and unstable APIs, we
|
||||||
|
were unsure and limiting the effort.
|
||||||
|
The plans is to support a wider range of extension points and guest languages,
|
||||||
|
thus supporting more use-cases and making it suitable for wider range of
|
||||||
|
developers.
|
||||||
|
If you think there's any language that would be particularly valuable, e.g. due
|
||||||
|
to its unique and important ecosystem, let us know.
|
||||||
|
When WASIp3 becomes available, hopefully in the near future, we're also
|
||||||
|
planning to transparently upgrade making asynchronous interactions between host
|
||||||
|
and guest more of a first-class citizen.
|
||||||
|
|
||||||
|
Thank you for making it this far and your time 🙏.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[^1]:
|
||||||
|
GLIBC static binaries aren't really static.
|
||||||
|
|
||||||
|
[^2]:
|
||||||
|
As system to express cross-component interfaces for WASM in a
|
||||||
|
language-agnostic manner. It's like gRPC but FFI, i.e. no I/O
|
||||||
|
thus allowing both synchronous and asynchronous interactions.
|
||||||
|
|
||||||
|
[^3]:
|
||||||
|
That said, bundling the interpreter with static input unlocks some
|
||||||
|
optimizations, e.g.
|
||||||
|
[Futamura projection using weval](https://github.com/bytecodealliance/weval).
|
||||||
|
|
||||||
|
[^4]:
|
||||||
|
For state sharing between requests you'll need should rely on SQLite or
|
||||||
|
KVStore. Note that even with long-lived V8 isolates that was already the case,
|
||||||
|
since state was only shared coincidentally within the same isolate, i.e.
|
||||||
|
subsequent requests may or may not be able to see that state.
|
||||||
|
|
||||||
|
[^5]:
|
||||||
|
A long-standing feature request for us has been to support hot-restart when
|
||||||
|
components change. We're still planning to get there. A separate watcher
|
||||||
|
process would re-build the WASM component and signal the `trail` binary to
|
||||||
|
reload the WASM component.
|
||||||
@@ -14,7 +14,7 @@ experience. Gani, the person behind it, is a mad scientist 🙏.
|
|||||||
|
|
||||||
From a distance PocketBase and TrailBase are both single-executables providing
|
From a distance PocketBase and TrailBase are both single-executables providing
|
||||||
almost identical feature sets: REST APIs, realtime updates, authentication, file
|
almost identical feature sets: REST APIs, realtime updates, authentication, file
|
||||||
storage, JavaScript (JS) runtimes, admin dashboard..., all on top of SQLite.
|
storage, a runtime for custom endpoints, admin dashboard..., all on top of SQLite.
|
||||||
|
|
||||||
For the sake of this comparison, we'll dive a little deeper to have a closer
|
For the sake of this comparison, we'll dive a little deeper to have a closer
|
||||||
look at their differences both technically and philosophically.
|
look at their differences both technically and philosophically.
|
||||||
@@ -59,8 +59,11 @@ Measuring we found that TrailBase's APIs are roughly [10x faster](/reference/ben
|
|||||||
This may sound like a lot but is the result of SQLite itself being extremely
|
This may sound like a lot but is the result of SQLite itself being extremely
|
||||||
fast meaning that even small overheads weigh heavily.
|
fast meaning that even small overheads weigh heavily.
|
||||||
|
|
||||||
Independently, TrailBase choice of V8 as its JS runtime allows code to run
|
Independently, TrailBase offers a WebAssembly runtime over PocketBase's
|
||||||
roughly 40x faster.
|
JavaScript interpreter.
|
||||||
|
This allows for strict state isolation between requests and supports a
|
||||||
|
wide-range of guest languages - currently JS/TS and Rust with more to come.
|
||||||
|
A custom Rust endpoint can be up to 140x faster than a PocketBase JS one.
|
||||||
|
|
||||||
### Framework Use
|
### Framework Use
|
||||||
|
|
||||||
@@ -94,10 +97,8 @@ sleeve:
|
|||||||
|
|
||||||
- Language independent bindings via JSON-schema with strict type-safety
|
- Language independent bindings via JSON-schema with strict type-safety
|
||||||
being enforced from the client all the way to the database[^4].
|
being enforced from the client all the way to the database[^4].
|
||||||
- A more Node-like JS runtime with full ES6 support, built-in TypeScript
|
- A WASM runtime, supporting multiple guest languages for custom endpoints and
|
||||||
transpilation, and V8 performance unlocking more of the JS ecosystem and enabling
|
up to 140x speed-up[^5].
|
||||||
<a href={githubPath("examples/collab-clicker-ssr")}>server-side rendering (SSR)</a>
|
|
||||||
with any popular JS framework.
|
|
||||||
- Untethered access to SQLite with all its features and capabilities.
|
- Untethered access to SQLite with all its features and capabilities.
|
||||||
- A wider set of first-class client libraries beyond JS/TS and Dart, including
|
- A wider set of first-class client libraries beyond JS/TS and Dart, including
|
||||||
C#, Python and Rust.
|
C#, Python and Rust.
|
||||||
@@ -154,3 +155,8 @@ flexibility and performance matter.
|
|||||||
[^4]:
|
[^4]:
|
||||||
Note that SQLite is not strictly typed by default. Instead column types
|
Note that SQLite is not strictly typed by default. Instead column types
|
||||||
merely a type affinity for value conversions.
|
merely a type affinity for value conversions.
|
||||||
|
|
||||||
|
[^5]:
|
||||||
|
Depends a lot on the guest language and the specific taks in question. 140x
|
||||||
|
is what we measured in heavy computational tasks comparing Goja with
|
||||||
|
Rust->WASM. It's apples to oranges, yet practically applicable.
|
||||||
|
|||||||
@@ -1,57 +1,41 @@
|
|||||||
---
|
---
|
||||||
title: JS/TS APIs
|
title: Custom APIs
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Aside } from "@astrojs/starlight/components";
|
import { Aside } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
On startup TrailBase will automatically load any JavaScript and TypeScript
|
On startup TrailBase will automatically load any WASM component, i.e. `*.wasm`
|
||||||
files in `traildepot/scripts`.
|
files in `traildepot/wasm`.
|
||||||
This can be used to implement arbitrary HTTP APIs using custom handlers.
|
This can be used to implement arbitrary HTTP APIs with custom handlers.
|
||||||
|
|
||||||
## Example HTTP Endpoint
|
## Example HTTP Endpoint
|
||||||
|
|
||||||
The following example illustrates a few things:
|
The following TypeScript WASM example illustrates a few things:
|
||||||
|
|
||||||
* How to register a parameterized route with `{table}`.
|
* How to register a parameterized route with `{table}`.
|
||||||
* How to implement a handler that returns `text/plain` content. There is also
|
|
||||||
`jsonHandler` and `htmlHandler`.
|
|
||||||
* How to query the database.
|
* How to query the database.
|
||||||
* How to return an error.
|
* How to return an HTTP error.
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import {
|
import { defineConfig } from "trailbase-wasm";
|
||||||
addRoute,
|
import { Request, HttpError, HttpHandler, StatusCode } from "trailbase-wasm/http";
|
||||||
query,
|
import { query } from "trailbase-wasm/db";
|
||||||
stringHandler,
|
|
||||||
HttpError,
|
|
||||||
StatusCodes
|
|
||||||
} from "../trailbase.js";
|
|
||||||
|
|
||||||
addRoute("GET", "/test/{table}", stringHandler(async (req) => {
|
async function handler(req: Request): Promise<string> {
|
||||||
const table = req.params["table"];
|
const table = req.getPathParam("table");
|
||||||
if (table) {
|
if (table) {
|
||||||
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
|
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
|
||||||
return `entries: ${rows[0][0]}`;
|
return `entries: ${rows[0][0]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
StatusCodes.BAD_REQUEST, "Missing '?table=' search query param");
|
StatusCode.BAD_REQUEST, "Missing '?table=' search query param");
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
httpHandlers: [ HttpHandler.get("/test/{table}", handler) ],
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
More examples can be found in the repository in
|
More examples can be found in the repository under `examples/wasm-guest-ts/`,
|
||||||
`client/testfixture/scripts/index.ts`.
|
`examples/wasm-guest-js/` and `examples/wasm-guest-rust/`.
|
||||||
|
|
||||||
## Runtime Details
|
|
||||||
|
|
||||||
At its heart, TrailBase's runtime is a pool of V8 worker threads - called
|
|
||||||
*isolates* - alongside a runtime that supports basic tasks such as file I/O, web
|
|
||||||
requests, timers, etc.
|
|
||||||
While it uses Deno's V8-integration under the hood, it is *not* a full Node.js
|
|
||||||
runtime, at least for now.
|
|
||||||
|
|
||||||
<Aside type="note" title="State Sharing">
|
|
||||||
Different *isolates* do not share state, i.e. you cannot use global state to
|
|
||||||
reliably share state across requests. Instead, state can be persisted using
|
|
||||||
the database.
|
|
||||||
</Aside>
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: >
|
|||||||
TypeScript API, Vector Search & UI
|
TypeScript API, Vector Search & UI
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Aside, Code } from "@astrojs/starlight/components";
|
import { Aside, Code, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
import { githubPath } from "@/lib/github";
|
import { githubPath } from "@/lib/github";
|
||||||
import { repo } from "@/config";
|
import { repo } from "@/config";
|
||||||
@@ -84,30 +84,38 @@ UPDATE coffee SET embedding = vec_f32(FORMAT("[%f, %f, %f, %f]", Aroma, Flavor,
|
|||||||
initializing the previously skipped `coffee.embedding` for all records.
|
initializing the previously skipped `coffee.embedding` for all records.
|
||||||
|
|
||||||
|
|
||||||
## Custom TypeScript Endpoint
|
## Custom Endpoints
|
||||||
|
|
||||||
Any time you start `trail run`[^3], JavaScript and TypeScript files under
|
Any time you start `trail run`[^3], WebAssembly components, i.e. `.wasm` files,
|
||||||
`traildepot/scripts` will be executed.
|
under `traildepot/wasm` will be compiled and initialized.
|
||||||
|
|
||||||
<Aside type="note">
|
We can use this to declare custom HTTP API routes among other things.
|
||||||
TrailBase will automatically transpile TypeScript to JavaScript which can
|
Let's have a quick look at `examples/coffee-vector-search/guests`,
|
||||||
then execute on the underlying V8 engine. You don't need a separate build
|
which define a `/search` API route using vector search, which we'll later need
|
||||||
step.
|
to find coffees most closely matching our desired coffee:
|
||||||
</Aside>
|
|
||||||
|
|
||||||
We can use this to register custom HTTP API routes among other things.
|
import handlerTsCode from "@root/examples/coffee-vector-search/guests/typescript/src/index.ts?raw";
|
||||||
Let's have a quick look at `examples/coffee-vector-search/traildepot/scripts/main.ts`,
|
import handlerRustCode from "@root/examples/coffee-vector-search/guests/rust/src/lib.rs?raw";
|
||||||
which defines a `/search` API route we'll later use in our application to
|
|
||||||
find coffees most closely matching our desired coffee notes:
|
|
||||||
|
|
||||||
import handlerCode from "@root/examples/coffee-vector-search/traildepot/scripts.delme/main.ts?raw";
|
<Tabs>
|
||||||
|
<TabItem label="TypeScript">
|
||||||
|
<Code
|
||||||
|
code={handlerTsCode}
|
||||||
|
lang="ts"
|
||||||
|
title={"examples/coffee-vector-search/guests/typescript/src/index.ts"}
|
||||||
|
mark={[]}
|
||||||
|
/>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
<Code
|
<TabItem label="Rust">
|
||||||
code={handlerCode}
|
<Code
|
||||||
lang="ts"
|
code={handlerRustCode}
|
||||||
title={"examples/coffee-vector-search/traildepot/scripts/main.ts"}
|
lang="rust"
|
||||||
mark={[]}
|
title={"examples/coffee-vector-search/guests/rust/src/lib.rs"}
|
||||||
/>
|
mark={[]}
|
||||||
|
/>
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
While `trail run` is up, we can test the public `/search` endpoint simply by
|
While `trail run` is up, we can test the public `/search` endpoint simply by
|
||||||
running:
|
running:
|
||||||
|
|||||||
@@ -55,15 +55,11 @@ export const demoLink = "https://demo.trailbase.io";
|
|||||||
* Rust: one of the lowest overhead languages,
|
* Rust: one of the lowest overhead languages,
|
||||||
* Axum: one of the fastest HTTP servers,
|
* Axum: one of the fastest HTTP servers,
|
||||||
* SQLite: one of the fastest full-SQL databases,
|
* SQLite: one of the fastest full-SQL databases,
|
||||||
* V8: one of the fastest JS engines.
|
* Wasmtime: compiles custom endpoints to efficient native code.
|
||||||
|
|
||||||
TrailBase's APIs are [11x faster than PocketBase's and almost 40x faster than SupaBase's
|
TrailBase's APIs are [11x faster than PocketBase's and almost 40x faster than SupaBase's
|
||||||
with a fraction of the footprint](/reference/benchmarks) allowing you to
|
with a fraction of the footprint](/reference/benchmarks) allowing you to
|
||||||
serve millions of customers from a tiny box.
|
serve millions of customers from a tiny box.
|
||||||
|
|
||||||
In terms of JS/TS performance, V8 is roughly
|
|
||||||
[40x faster](/reference/benchmarks#javascript)
|
|
||||||
than goja used by PocketBase.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="second">
|
<div slot="second">
|
||||||
@@ -184,8 +180,8 @@ export const demoLink = "https://demo.trailbase.io";
|
|||||||
<Card title="APIs & File Storage" icon="random">
|
<Card title="APIs & File Storage" icon="random">
|
||||||
Provide access to your tables and views through fast, flexible and
|
Provide access to your tables and views through fast, flexible and
|
||||||
**type-safe** restful CRUD APIs.
|
**type-safe** restful CRUD APIs.
|
||||||
Listen for data changes with realtime APIs and extend functionality
|
Listen for data changes with realtime APIs and extend functionality using
|
||||||
using a fast V8 JS/ES6 runtime with built-in support for TypeScript.
|
a fast WebAssembly runtime with support for many guest languages.
|
||||||
|
|
||||||
Authorize users based on ACLs and SQL access rules letting you
|
Authorize users based on ACLs and SQL access rules letting you
|
||||||
easily build higher-level access management or moderation facilities
|
easily build higher-level access management or moderation facilities
|
||||||
|
|||||||
1
docs/src/content/docs/reference/_benchmarks/wasm_runtime.tsx
Symbolic link
1
docs/src/content/docs/reference/_benchmarks/wasm_runtime.tsx
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../blog/_switching_to_a_wasm_runtime.tsx
|
||||||
@@ -187,6 +187,28 @@ being roughly 3 times slower than p50.
|
|||||||
Slower insertions can take north of 100ms. This may be related to GC pauses,
|
Slower insertions can take north of 100ms. This may be related to GC pauses,
|
||||||
scheduling, or more generally the same CPU variability we observed earlier.
|
scheduling, or more generally the same CPU variability we observed earlier.
|
||||||
|
|
||||||
|
## Runtimes
|
||||||
|
|
||||||
|
[TrailBase is currently going through a transition from a V8-based runtime to a
|
||||||
|
WASM one](/blog/switching_to_a_wasm_runtime).
|
||||||
|
|
||||||
|
V8's execution with just-in-time (JIT) compilation is quite speedy and is about
|
||||||
|
40x faster than Goja, PocketBase's interpreter and similar other interpreters
|
||||||
|
(the graph's y-axis is logarithmic).
|
||||||
|
With the new WASM-based runtime, execution of JS relies on bundling an
|
||||||
|
interpreter and thus performance has regressed to be roughly on-par with Goja
|
||||||
|
and other JIT-less engines.
|
||||||
|
However, guests languages that can compile natively to WASM, e.g. Rust, can shine
|
||||||
|
with roughly a 135x speed-up with strong state isolation between requests.
|
||||||
|
|
||||||
|
import { RuntimeFib40Times } from "./_benchmarks/wasm_runtime.tsx";
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="h-[300px] w-[90%]">
|
||||||
|
<RuntimeFib40Times client:only="solid-js" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
## File System
|
## File System
|
||||||
|
|
||||||
File systems play an important role for the performance of storage systems,
|
File systems play an important role for the performance of storage systems,
|
||||||
@@ -230,6 +252,8 @@ disc space and feature sets. For example, CoW snapshots may or may not be
|
|||||||
important to you.
|
important to you.
|
||||||
|
|
||||||
|
|
||||||
|
{/*
|
||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
The benchmark sets up a custom HTTP endpoint `/fibonacci?n=<N>` using the same
|
The benchmark sets up a custom HTTP endpoint `/fibonacci?n=<N>` using the same
|
||||||
@@ -243,12 +267,6 @@ In other words, the impact of any overhead within PocketBase or TrailBase is
|
|||||||
diminished by the time it takes to compute `fibonacci(N)` for sufficiently
|
diminished by the time it takes to compute `fibonacci(N)` for sufficiently
|
||||||
large `N`.
|
large `N`.
|
||||||
|
|
||||||
{/*
|
|
||||||
Output:
|
|
||||||
TB: Called "/fibonacci" for fib(40) 100 times, took 0:00:14.988703 (limit=64)
|
|
||||||
PB: Called "/fibonacci" for fib(40) 100 times, took 0:10:01.096053 (limit=64)
|
|
||||||
*/}
|
|
||||||
|
|
||||||
We found that for `N=40`, V8 (TrailBase) is around 40 times faster than
|
We found that for `N=40`, V8 (TrailBase) is around 40 times faster than
|
||||||
goja (PocketBase):
|
goja (PocketBase):
|
||||||
|
|
||||||
@@ -270,7 +288,15 @@ With the addition of V8 to TrailBase, we've experienced a significant increase
|
|||||||
in the memory baseline dominating the overall footprint.
|
in the memory baseline dominating the overall footprint.
|
||||||
In this setup, TrailBase consumes roughly 4 times more memory than PocketBase.
|
In this setup, TrailBase consumes roughly 4 times more memory than PocketBase.
|
||||||
If memory footprint is a major concern for you, constraining the number of V8
|
If memory footprint is a major concern for you, constraining the number of V8
|
||||||
threads will be an effective remedy (`--js-runtime-threads`).
|
threads will be an effective remedy (`--runtime-threads`).
|
||||||
|
|
||||||
|
*/}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Output:
|
||||||
|
TB: Called "/fibonacci" for fib(40) 100 times, took 0:00:14.988703 (limit=64)
|
||||||
|
PB: Called "/fibonacci" for fib(40) 100 times, took 0:10:01.096053 (limit=64)
|
||||||
|
*/}
|
||||||
|
|
||||||
## Final Words
|
## Final Words
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function getPosts() {
|
|||||||
const posts = blogPosts ?? (await getPosts());
|
const posts = blogPosts ?? (await getPosts());
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base title="Updates" draft={true}>
|
<Base title="Updates">
|
||||||
<section class="container mx-auto mb-10 max-w-screen-lg px-7 py-4">
|
<section class="container mx-auto mb-10 max-w-screen-lg px-7 py-4">
|
||||||
{
|
{
|
||||||
posts.map((post, index: number) => {
|
posts.map((post, index: number) => {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const proseStyle: string[] = [
|
|||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base title={entry.data.title} description={entry.data.intro} draft={true}>
|
<Base title={entry.data.title} description={entry.data.intro}>
|
||||||
<article>
|
<article>
|
||||||
{/* Header stuff: title, subtitle, tags, read time, ... */}
|
{/* Header stuff: title, subtitle, tags, read time, ... */}
|
||||||
<div class:list={[...proseStyle]}>
|
<div class:list={[...proseStyle]}>
|
||||||
@@ -80,7 +80,7 @@ const proseStyle: string[] = [
|
|||||||
<PublishDate date={entry.data.pubDate} />
|
<PublishDate date={entry.data.pubDate} />
|
||||||
|
|
||||||
<span class:list={["text-[16px]", "transition-all", "duration-300"]}>
|
<span class:list={["text-[16px]", "transition-all", "duration-300"]}>
|
||||||
~{Math.floor(readMinutes) + 1} minutes
|
~{Math.floor(readMinutes) + 1} minute read
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user