makepad/libs/asset/store/examples/gc_probe.rs
Admin 2d23dba736 libs/asset: the store runs on our own SQLite, and the importers learn the whole map contract
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.
2026-08-23 01:34:34 +02:00

112 lines
4.2 KiB
Rust

//! Measure (and optionally perform) blob garbage collection on a server
//! root: how many blobs and bytes are unreachable, how long the incremental
//! run takes, and what one step costs.
//!
//! Opening a root migrates its schema, and `--collect` DELETES bytes. Point
//! it at a COPY of a live root, never at one a server is using.
//!
//! cargo run --release -p makepad-asset-store --example gc_probe -- <root>
//! cargo run --release -p makepad-asset-store --example gc_probe -- <root> --retain 1
//! cargo run --release -p makepad-asset-store --example gc_probe -- <root> --collect
//!
//! Default is a DRY RUN: nothing is deleted and nothing is retired.
//! `--retain N` previews (or applies) the retention rule that keeps the
//! newest N revisions per asset plus every alias head.
use makepad_asset_store::{AssetServerCore, Budgets, GcConfig, GcStatus};
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: gc_probe <server-root> [--collect] [--retain N] [--grace-ms N]");
std::process::exit(2);
}
};
let mut cfg = GcConfig { dry_run: true, grace_ms: 0, ..GcConfig::default_v1() };
while let Some(flag) = args.next() {
match flag.as_str() {
"--collect" => cfg.dry_run = false,
"--retain" => {
let n: u32 = args.next().and_then(|v| v.parse().ok()).expect("--retain N");
cfg.retain_keep = Some(n);
}
"--grace-ms" => {
cfg.grace_ms = args.next().and_then(|v| v.parse().ok()).expect("--grace-ms N");
}
other => {
eprintln!("unknown flag {other}");
std::process::exit(2);
}
}
}
let t0 = Instant::now();
let core = AssetServerCore::open(&root, Budgets::default_v1()).expect("open root");
println!("open + migrate: {:?}", t0.elapsed());
let recovered = core.recover(now_ms()).expect("recover");
println!(
"recover: {} cas temps, {} pending deletes, {} leases",
recovered.cas_temps_removed, recovered.gc_deletes_resolved, recovered.leases_expired
);
let now = now_ms();
println!(
"mode: {} retain={:?} grace_ms={}",
if cfg.dry_run { "DRY RUN" } else { "COLLECT" },
cfg.retain_keep,
cfg.grace_ms
);
core.gc_begin(cfg, now).expect("gc begin");
let t0 = Instant::now();
let mut steps = 0u64;
let mut worst = Duration::ZERO;
let status: GcStatus = loop {
let t = Instant::now();
let status = core.gc_advance(1, now).expect("gc step").expect("gc run");
let took = t.elapsed();
steps += 1;
worst = worst.max(took);
if steps % 200 == 0 {
println!(
" .. step {steps} phase={} scanned={} marked={} examined={} freed={} bytes",
status.phase.as_str(),
status.scanned_revisions,
status.marked_blobs,
status.examined_blobs,
status.unreferenced_bytes
);
}
if status.finished() {
break status;
}
};
println!("run {} in {:?} over {steps} steps (worst step {worst:?})", status.run_id, t0.elapsed(), );
println!(" phase {}", status.phase.as_str());
println!(" retired revisions {}", status.retired_revisions);
println!(" documents scanned {}", status.scanned_revisions);
println!(" blobs referenced {}", status.marked_blobs);
println!(" blobs examined {}", status.examined_blobs);
println!(
" unreferenced {} blobs, {} bytes ({:.1} MiB)",
status.unreferenced_blobs,
status.unreferenced_bytes,
status.unreferenced_bytes as f64 / (1024.0 * 1024.0)
);
println!(
" deleted {} blobs, {} bytes ({:.1} MiB)",
status.deleted_blobs,
status.deleted_bytes,
status.deleted_bytes as f64 / (1024.0 * 1024.0)
);
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_millis() as u64
}