The asset store now uses libs/sqlite_query as its ONLY engine — not a feature
flag, not a fallback. That closes the Windows gap (the embedded store starts
there now, and a SHARED->EXCLUSIVE upgrade is handled rather than assumed
free) and takes the C dependency out of the build everywhere else.
Around it:
- store: a garbage collector, catalogued content that is referenced in place
instead of copied, the `vjeffect` kind, and host/chat routes that keep up
with the chat wire below.
- importer: the unified map contract reaches quake2, quake3, doom and duke —
world placement, nav, welding, prelit maps and glTF node handling shared
rather than reimplemented per game. Music import, billboards and stateful
props move to the data crate so readers stop linking the importer.
- ai: the serving side of multi-lane chat — per-lane conversations, honest
progress and acceptance reporting, penalties and a watchdog, context as a
per-box number that compacts instead of erupting, a realtime session mode,
and inpaint/flux2 backends. `chat_bench` measures the rate the way the
client meter computes it.
- client / chat / chat_ui: a publication can NAME a file instead of carrying
it; the wire says whether a turn is warm and whether it is thinking, so a
client stops guessing; transcript and feed widgets render history the way
the model wrote it. `SessionConfig::catalog_runtime` lets a host size the
catalog runtime's lanes itself — a browsing UI puts every listing, every
per-tile resolve and every thumbnail blob through that one runtime and
wants a wider fast lane than the shared default, while media lanes keep
it (a few big transfers, not a thousand small ones).
- widgets: the shared asset widgets — one video view (knobbed seek,
transport, bracket trim, rail playback) used everywhere, plus thumb,
preview, scene view, walk-world and the lyric reader.
216 lines
8.1 KiB
Rust
216 lines
8.1 KiB
Rust
//! What a thumbnail grid costs, measured three ways against a real server.
|
|
//!
|
|
//! Serves a COPY of a server root on ephemeral ports, picks N thumbnails out
|
|
//! of the catalog, and fetches them with a cold client cache under three
|
|
//! transport configurations:
|
|
//!
|
|
//! 1. `connect-per-request` — one TCP connect + request + close per blob
|
|
//! (what this client did before keep-alive existed)
|
|
//! 2. `keep-alive` — one connection, N requests
|
|
//! 3. `keep-alive + batch` — one connection, ONE ordered request
|
|
//!
|
|
//! Point it at a copy, never at a root a server is using:
|
|
//!
|
|
//! cargo run --release -p makepad-asset-store --example thumb_bench -- <root> [count]
|
|
|
|
use makepad_asset_client::{
|
|
ApiEndpoints, AssetClient, ClientConfig, ClientEvent, ClientOutput, ClientRequest,
|
|
ClientRuntime, HttpLimits, RuntimeConfig, SubmitOptions,
|
|
};
|
|
use makepad_asset_data::{AssetManifest, BlobId};
|
|
use makepad_asset_store::{AssetServer, ServerConfig};
|
|
use std::path::PathBuf;
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
let root = match args.next() {
|
|
Some(r) => PathBuf::from(r),
|
|
None => {
|
|
eprintln!("usage: thumb_bench <server-root-copy> [count]");
|
|
std::process::exit(2);
|
|
}
|
|
};
|
|
let count: usize = args.next().and_then(|v| v.parse().ok()).unwrap_or(30);
|
|
|
|
let mut cfg = ServerConfig::new(root.clone());
|
|
cfg.control_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.data_addr = "127.0.0.1:0".parse().unwrap();
|
|
cfg.bootstrap_admin = true;
|
|
cfg.log = false;
|
|
let mut server = AssetServer::start(cfg).expect("server start");
|
|
let token = std::fs::read_to_string(root.join("admin-token"))
|
|
.expect("admin token")
|
|
.trim()
|
|
.to_string();
|
|
let endpoints = ApiEndpoints { control: server.control_addr(), data: server.data_addr() };
|
|
|
|
// Collect N real thumbnails (blob + declared length) from the catalog.
|
|
let mut probe = connect(&endpoints, &token, "probe", true);
|
|
let page = probe
|
|
.catalog_search(
|
|
&makepad_asset_client::CatalogQuery::browse(
|
|
(count as u32).saturating_mul(3).clamp(8, makepad_asset_client::MAX_SEARCH_LIMIT),
|
|
),
|
|
None,
|
|
)
|
|
.expect("browse");
|
|
let mut thumbs: Vec<(BlobId, u64)> = Vec::new();
|
|
let (mut no_alias, mut no_head, mut no_manifest, mut no_thumb) = (0, 0, 0, 0);
|
|
for hit in &page.hits {
|
|
// Resolve through the alias head: that is the revision a UI shows.
|
|
let Some(alias) = hit.alias.as_ref() else {
|
|
no_alias += 1;
|
|
continue;
|
|
};
|
|
let head = match probe.resolve_alias(alias) {
|
|
Ok(h) => h,
|
|
Err(_) => {
|
|
no_head += 1;
|
|
continue;
|
|
}
|
|
};
|
|
let manifest = match probe.fetch_asset_manifest(&head.head_revision) {
|
|
Ok(m) => m,
|
|
Err(_) => {
|
|
no_manifest += 1;
|
|
continue;
|
|
}
|
|
};
|
|
match thumbnail_of(&manifest) {
|
|
Some(t) => thumbs.push(t),
|
|
None => no_thumb += 1,
|
|
}
|
|
if thumbs.len() == count {
|
|
break;
|
|
}
|
|
}
|
|
if thumbs.is_empty() {
|
|
eprintln!(
|
|
"no thumbnails found in {} (hits: {}, with alias: {})",
|
|
root.display(),
|
|
page.hits.len(),
|
|
page.hits.iter().filter(|h| h.alias.is_some()).count()
|
|
);
|
|
eprintln!(
|
|
"skipped: no_alias {no_alias}, alias unresolved {no_head}, manifest {no_manifest}, no thumbnail {no_thumb}"
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
let total_bytes: u64 = thumbs.iter().map(|(_, len)| *len).sum();
|
|
println!(
|
|
"{} thumbnails, {} bytes total ({} avg)",
|
|
thumbs.len(),
|
|
total_bytes,
|
|
total_bytes / thumbs.len() as u64
|
|
);
|
|
|
|
// Baseline without the runtime in the picture: sequential fetches on one
|
|
// handle, so a per-request cost cannot hide behind worker scheduling.
|
|
for (label, keep_alive) in [("sequential, connect-per-request", false), ("sequential, keep-alive", true)] {
|
|
let before = server.data_connections_accepted();
|
|
let before_reqs = server.data_requests_served();
|
|
let mut client = connect(&endpoints, &token, label, keep_alive);
|
|
let t0 = Instant::now();
|
|
for (blob, len) in &thumbs {
|
|
client.fetch_blob(blob, Some(*len), None).expect("fetch");
|
|
}
|
|
let elapsed = t0.elapsed();
|
|
println!(
|
|
"{label:>32}: {:>8.2} ms total, {:>6.3} ms each, {} conn, {} requests",
|
|
elapsed.as_secs_f64() * 1000.0,
|
|
elapsed.as_secs_f64() * 1000.0 / thumbs.len() as f64,
|
|
server.data_connections_accepted() - before,
|
|
server.data_requests_served() - before_reqs,
|
|
);
|
|
}
|
|
|
|
for (label, keep_alive, batch) in [
|
|
("connect-per-request", false, 1usize),
|
|
("keep-alive", true, 1),
|
|
("keep-alive + batch", true, 16),
|
|
] {
|
|
let before = server.data_connections_accepted();
|
|
let before_reqs = server.data_requests_served();
|
|
let client = connect(&endpoints, &token, label, keep_alive);
|
|
let mut runtime = ClientRuntime::start_with(
|
|
client,
|
|
RuntimeConfig { fast_batch_max_items: batch, ..RuntimeConfig::default_v1() },
|
|
)
|
|
.expect("runtime");
|
|
let t0 = Instant::now();
|
|
let mut ids = Vec::with_capacity(thumbs.len());
|
|
for (blob, len) in &thumbs {
|
|
ids.push(
|
|
runtime
|
|
.submit_with(
|
|
ClientRequest::FetchBlob {
|
|
blob: *blob,
|
|
expected_len: Some(*len),
|
|
pin: false,
|
|
},
|
|
SubmitOptions::fast(),
|
|
)
|
|
.expect("submit"),
|
|
);
|
|
}
|
|
let mut done = 0usize;
|
|
let deadline = Instant::now() + Duration::from_secs(120);
|
|
while done < ids.len() {
|
|
assert!(Instant::now() < deadline, "{label}: never finished");
|
|
for event in runtime.poll() {
|
|
match event {
|
|
ClientEvent::Done { output: ClientOutput::Blob { .. }, .. } => done += 1,
|
|
ClientEvent::Failed { id, error } => panic!("{label}: {id} failed: {error}"),
|
|
_ => {}
|
|
}
|
|
}
|
|
std::thread::sleep(Duration::from_millis(1));
|
|
}
|
|
let elapsed = t0.elapsed();
|
|
let conns = server.data_connections_accepted() - before;
|
|
let reqs = server.data_requests_served() - before_reqs;
|
|
println!(
|
|
"{label:>32}: {:>8.2} ms total, {:>6.3} ms each, {conns} conn, {reqs} requests",
|
|
elapsed.as_secs_f64() * 1000.0,
|
|
elapsed.as_secs_f64() * 1000.0 / thumbs.len() as f64,
|
|
);
|
|
runtime.shutdown();
|
|
}
|
|
server.shutdown();
|
|
}
|
|
|
|
/// The bytes a grid actually renders for one asset: its typed thumbnail
|
|
/// when it has one, otherwise the smallest thumbnail-sized file it carries
|
|
/// (sprite/billboard content has no separate thumbnail — the sprite IS it).
|
|
fn thumbnail_of(manifest: &AssetManifest) -> Option<(BlobId, u64)> {
|
|
if let Some(t) = manifest.thumbnail.as_ref() {
|
|
return Some((t.blob, t.byte_len));
|
|
}
|
|
manifest
|
|
.files
|
|
.iter()
|
|
.filter(|f| f.byte_len <= 512 * 1024)
|
|
.min_by_key(|f| f.byte_len)
|
|
.map(|f| (f.blob, f.byte_len))
|
|
}
|
|
|
|
fn connect(endpoints: &ApiEndpoints, token: &str, tag: &str, keep_alive: bool) -> AssetClient {
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"mp_thumb_bench_{}_{}_{}",
|
|
std::process::id(),
|
|
tag.replace(' ', "_").replace('+', ""),
|
|
Instant::now().elapsed().as_nanos()
|
|
));
|
|
let mut cfg = ClientConfig::new(dir);
|
|
cfg.token = Some(token.to_string());
|
|
cfg.http_keep_alive = keep_alive;
|
|
cfg.http = HttpLimits {
|
|
connect_timeout_ms: 2_000,
|
|
read_timeout_ms: 10_000,
|
|
write_timeout_ms: 10_000,
|
|
head_deadline_ms: 10_000,
|
|
body_deadline_ms: 60_000,
|
|
};
|
|
AssetClient::connect(cfg, *endpoints, None).expect("connect")
|
|
}
|