perf(studio): speed up nitro scan and stop the cleanup panel freezing
CI / check (push) Successful in 4m13s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m0s

This commit is contained in:
openhands committed 2026-09-19 11:39:19 +02:00
1 parent 91e649398c
commit c916e42572
2 files changed
+40 -5

No files matched your search

@@ -63,6 +63,13 @@ type AssetKind = "nitro" | "swf" | "icon";
const DAY_MS = 24 * 60 * 60 * 1000;
/**
* How many rows a group renders at once. Directories can contain tens of
* thousands of leftover files; mounting every row as DOM nodes freezes the
* tab, so rows are added in chunks via a "load more" button.
*/
const RENDER_CHUNK = 500;
function ageLabel(lastModified: number): string {
if (!lastModified) return "age unknown";
const days = Math.floor((Date.now() - lastModified) / DAY_MS);
@@ -103,8 +110,11 @@ function CleanupGroup({
onToggle: (fileName: string) => void;
onToggleAll: (names: string[]) => void;
}) {
const [shown, setShown] = useState(RENDER_CHUNK);
const checked = entries.filter((e) => selected.has(e.fileName));
const allChecked = entries.length > 0 && checked.length === entries.length;
const visibleEntries = entries.slice(0, shown);
const toneClass =
tone === "error"
? "border-[var(--admin-error-border)] bg-[var(--admin-error-subtle)]"
@@ -148,8 +158,9 @@ function CleanupGroup({
No {label.toLowerCase()} found
</p>
) : (
<>
<ul className="max-h-80 space-y-1 overflow-auto rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-2">
{entries.map((entry) => (
{visibleEntries.map((entry) => (
<li
key={entry.fileName}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-[var(--admin-accent)]/5"
@@ -187,6 +198,23 @@ function CleanupGroup({
</li>
))}
</ul>
{entries.length > shown && (
<div className="flex items-center justify-between px-2 text-xs text-[var(--admin-text-muted)]">
<span>
Showing {shown} of {entries.length}
</span>
<Button
type="button"
variant="outline"
size="sm"
disabled={disabled}
onClick={() => setShown((s) => s + RENDER_CHUNK)}
>
Load {Math.min(RENDER_CHUNK, entries.length - shown)} more
</Button>
</div>
)}
</>
)}
</section>
);
+11 -4
View File
@@ -217,14 +217,21 @@ interface DirectoryRead {
lastModified: number;
}
/**
* How many `stat` calls run in parallel while reading an asset directory.
* Directories can hold hundreds of thousands of files; serial `stat`ing them
* is the single biggest cost of the scan, so the work is spread over a pool.
*/
const DIR_STAT_CONCURRENCY = 32;
async function readAssetDir(
dir: string,
regex: RegExp,
): Promise<DirectoryRead[]> {
const names = await fs.readdir(dir).catch(() => [] as string[]);
const entries: DirectoryRead[] = [];
for (const name of names) {
if (!regex.test(name)) continue;
await runPool(names, DIR_STAT_CONCURRENCY, async (name) => {
if (!regex.test(name)) return;
let size = 0;
let mtimeMs = 0;
try {
@@ -240,7 +247,7 @@ async function readAssetDir(
size,
lastModified: mtimeMs,
});
}
});
return entries;
}
@@ -321,7 +328,7 @@ export async function scanFakeBrokenNitros(): Promise<NitroCleanupScan> {
const fake: NitroCleanupEntry[] = [];
const broken: NitroCleanupEntry[] = [];
await runPool(entries, 8, async (entry) => {
await runPool(entries, 16, async (entry) => {
// A .nitro that no DB item maps to is a leftover / fake bundle.
if (!validStems.has(entry.base.toLowerCase())) {
fake.push(entry);