Skip to content

Commit 809e8a3

Browse files
ralyodioclaude
andauthored
feat(install): clear bloated profile caches on upgrade, and add tron clean (#74)
Chromium's disk caches have no ceiling that matters here. Service-worker CacheStorage is quota-managed per origin, so a handful of heavy sites can carry a profile into the gigabytes on their own. Past roughly a gigabyte the cost stops being disk and starts being latency: the CacheStorage index is consulted on navigation, and once bloated, scrolling and tab switching go with it. The failure is gradual and then sudden, so it reads as "the browser broke today" — a profile that had reached 3.9G, with 1.4G of CacheStorage, is what prompted this. Clearing three directories took it to 534M and the sluggishness went away. `tron upgrade` now clears Cache, Code Cache and Service Worker once they exceed TRONBROWSER_CACHE_LIMIT_MB (default 1024, 0 disables). None of those hold bookmarks, passwords, history or cookies, so it costs a re-download and nothing else — you stay logged in everywhere, which is what makes this better than chrome://settings/clearBrowserData, where clearing service workers means clearing cookies too. The threshold matters: the auto-upgrade check runs daily, and clearing a healthy profile every day would cost everyone a re-download for nothing. `tron clean` does it on demand, implemented in the CLI rather than by re-running the installer — cleaning a bloated profile is exactly when you don't want to need the network. Neither path will unlink anything while the browser is running; the profile is memory-mapped, and pulling it out from under Chromium yields a corrupt profile rather than a clean one. Tests cover the cleanup against a fake profile: caches go, bookmarks, cookies, history, login data and IndexedDB stay, the threshold and the kill switch are honored, and TRONBROWSER_DATA is respected. The `tron` CLI's copy is generated inside a heredoc, so `sh -n` never sees it — it was extracted and checked with sh, dash, and a live run. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent aafbaf9 commit 809e8a3

2 files changed

Lines changed: 225 additions & 3 deletions

File tree

apps/web/public/install.sh

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Usage:
9696
tron trace start|stop Record commands into a .trontrace bundle
9797
tron replay <bundle> Replay a recorded trace against the session
9898
tron upgrade Update to the latest release
99+
tron clean Clear browser caches (keeps bookmarks, logins, history)
99100
tron remove Uninstall TronBrowser (keeps your profile data)
100101
tron version Print the installed version
101102
tron help Show this help
@@ -249,6 +250,24 @@ case "${1:-}" in
249250
exec "$TORBIN" --SocksPort 127.0.0.1:9071 --DataDirectory "$TOR_DATA" ;;
250251
upgrade|update)
251252
exec sh -c "curl -fsSL '$INSTALL_URL' | sh -s -- upgrade" ;;
253+
clean)
254+
# `tron upgrade` clears these too, once they pass a size threshold — see
255+
# prune_profile_caches in install.sh. This copy is deliberate: cleaning a
256+
# bloated profile is exactly when you don't want to need the network.
257+
# Regenerable caches only; bookmarks, passwords, history and cookies stay.
258+
for _data in "${TRONBROWSER_DATA:-$HOME/.tronbrowser}" "$HOME/TronBrowser"; do
259+
[ -d "$_data/Default" ] || continue
260+
if command -v pgrep >/dev/null 2>&1 && pgrep -f "user-data-dir=$_data" >/dev/null 2>&1; then
261+
echo "TronBrowser is running — quit it first, then run 'tron clean'." >&2
262+
continue
263+
fi
264+
_freed="$(du -sm "$_data/Default/Cache" "$_data/Default/Code Cache" \
265+
"$_data/Default/Service Worker" 2>/dev/null \
266+
| awk '{t += $1} END {print t + 0}')"
267+
[ "${_freed:-0}" -gt 0 ] || continue
268+
rm -rf "$_data/Default/Cache" "$_data/Default/Code Cache" "$_data/Default/Service Worker"
269+
echo "Freed ~${_freed}MB from $_data. Bookmarks, passwords and logins untouched."
270+
done ;;
252271
remove|uninstall)
253272
rm -rf "$APP_DIR"
254273
rm -f "$PREFIX/bin/tron" "$PREFIX/bin/tronbrowser" "$PREFIX/share/applications/tronbrowser.desktop"
@@ -586,7 +605,83 @@ DESKTOP
586605
esac
587606
}
588607

608+
# Clear the profile's regenerable caches once they get big enough to hurt.
609+
#
610+
# Chromium's disk caches have no ceiling that matters here. Service-worker
611+
# CacheStorage in particular is quota-managed per origin, so a handful of heavy
612+
# sites can carry a profile into the gigabytes on their own. Past roughly a
613+
# gigabyte the cost stops being disk and starts being latency: the CacheStorage
614+
# index is consulted on navigation, and once it is bloated, scrolling and tab
615+
# switching go with it. The failure is gradual and then sudden, which makes it
616+
# read as "the browser broke today" — a profile that had grown to 3.9G, with
617+
# 1.4G of CacheStorage, is what prompted this.
618+
#
619+
# None of these three directories holds bookmarks, passwords, history or
620+
# cookies, so clearing them costs a re-download and nothing else. Upgrade is the
621+
# right moment: it is the one command every user already runs, and the browser
622+
# is usually closed for it.
623+
#
624+
# `$1` = "force" to clear regardless of size (that is `tron clean`).
625+
# TRONBROWSER_CACHE_LIMIT_MB=0 disables the automatic pass entirely.
626+
PROFILE_CACHES="Cache
627+
Code Cache
628+
Service Worker"
629+
630+
dir_mb() {
631+
[ -d "$1" ] || { echo 0; return 0; }
632+
_mb="$(du -sm "$1" 2>/dev/null | awk 'NR==1{print $1}')"
633+
echo "${_mb:-0}"
634+
}
635+
636+
prune_profile_caches() {
637+
_force="${1:-}"
638+
_limit="${TRONBROWSER_CACHE_LIMIT_MB:-1024}"
639+
if [ "$_force" != "force" ] && [ "$_limit" = "0" ]; then
640+
return 0
641+
fi
642+
643+
for _data in "${TRONBROWSER_DATA:-$HOME/.tronbrowser}" "$HOME/TronBrowser"; do
644+
[ -d "$_data/Default" ] || continue
645+
646+
# Never unlink these under a live browser. The profile is memory-mapped, and
647+
# pulling it out from underneath Chromium gives you a corrupt profile rather
648+
# than a clean one.
649+
if command -v pgrep >/dev/null 2>&1 && pgrep -f "user-data-dir=$_data" >/dev/null 2>&1; then
650+
warn "TronBrowser is running — leaving $_data alone. Quit it, then run 'tron clean'."
651+
continue
652+
fi
653+
654+
_total=0
655+
_old_ifs="$IFS"; IFS="
656+
"
657+
for _c in $PROFILE_CACHES; do
658+
_total=$((_total + $(dir_mb "$_data/Default/$_c")))
659+
done
660+
IFS="$_old_ifs"
661+
662+
if [ "$_force" != "force" ] && [ "$_total" -lt "$_limit" ]; then
663+
continue
664+
fi
665+
if [ "$_total" -eq 0 ]; then
666+
continue
667+
fi
668+
669+
info "Clearing ${_total}MB of browser cache from $_data (bookmarks, passwords, history and logins are untouched)."
670+
_old_ifs="$IFS"; IFS="
671+
"
672+
for _c in $PROFILE_CACHES; do
673+
rm -rf "$_data/Default/$_c"
674+
done
675+
IFS="$_old_ifs"
676+
say "Freed ~${_total}MB. Cached assets re-download on demand; push notifications need re-granting."
677+
done
678+
}
679+
589680
do_upgrade() {
681+
# Before anything else, so a running browser is reported while the user is
682+
# still watching, and so the check happens even when already up to date.
683+
prune_profile_caches
684+
590685
if [ ! -f "$VERSION_FILE" ]; then
591686
warn "TronBrowser not installed; installing fresh."
592687
do_install
@@ -628,22 +723,33 @@ Usage: curl -fsSL $INSTALL_URL | sh [-s -- <command>]
628723
Commands:
629724
install Download and install the latest TronBrowser (default)
630725
upgrade Update an existing install to the latest release
726+
clean Clear the profile's browser caches (keeps bookmarks/logins).
727+
'clean --if-large' only acts past TRONBROWSER_CACHE_LIMIT_MB
631728
remove Uninstall TronBrowser (keeps your profile data)
632729
version Print the installed version
633730
help Show this help
634731
635-
After install, prefer the 'tron' CLI: tron upgrade | tron remove | tron version
732+
After install, prefer the 'tron' CLI: tron upgrade | tron clean | tron remove
636733
637734
Env:
638-
TRONBROWSER_PREFIX install prefix (default: \$HOME/.local)
639-
TRONBROWSER_REPO GitHub repo (default: $REPO)
735+
TRONBROWSER_PREFIX install prefix (default: \$HOME/.local)
736+
TRONBROWSER_REPO GitHub repo (default: $REPO)
737+
TRONBROWSER_CACHE_LIMIT_MB clear profile caches on upgrade once they exceed
738+
this (default: 1024; 0 disables)
640739
EOF
641740
}
642741

643742
cmd="${1:-install}"
644743
case "$cmd" in
645744
install) do_install ;;
646745
upgrade|update) do_upgrade ;;
746+
clean)
747+
# Bare `clean` always clears; --if-large respects TRONBROWSER_CACHE_LIMIT_MB
748+
# and is the pass `tron upgrade` runs for you.
749+
case "${2:-}" in
750+
--if-large) prune_profile_caches ;;
751+
*) prune_profile_caches force ;;
752+
esac ;;
647753
remove|uninstall) do_remove ;;
648754
ensure-tor) ensure_tor ;;
649755
version|--version|-v) do_version ;;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// install.sh's cache cleanup deletes directories out of a user's real profile,
2+
// so it gets tested against a fake one. The rule it has to hold to: regenerable
3+
// caches go, everything the user would miss stays.
4+
5+
import { spawnSync } from 'node:child_process';
6+
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, rmSync } from 'node:fs';
7+
import { tmpdir } from 'node:os';
8+
import { dirname, join } from 'node:path';
9+
import { fileURLToPath } from 'node:url';
10+
import { afterAll, describe, expect, it } from 'vitest';
11+
12+
const HERE = dirname(fileURLToPath(import.meta.url));
13+
const INSTALL_SH = join(HERE, '..', 'public', 'install.sh');
14+
15+
const CACHES = ['Cache', 'Code Cache', 'Service Worker'];
16+
const KEEP = ['Bookmarks', 'Cookies', 'History', 'Login Data'];
17+
18+
const roots: string[] = [];
19+
20+
/** A profile whose three cache directories hold roughly `mb` megabytes each. */
21+
function profile(mb: number): { home: string; data: string } {
22+
const home = mkdtempSync(join(tmpdir(), 'tron-clean-'));
23+
roots.push(home);
24+
const data = join(home, '.tronbrowser');
25+
const def = join(data, 'Default');
26+
27+
for (const c of CACHES) {
28+
mkdirSync(join(def, c), { recursive: true });
29+
if (mb > 0) writeFileSync(join(def, c, 'blob'), Buffer.alloc(mb * 1024 * 1024, 1));
30+
}
31+
// The things a user would be upset to lose.
32+
for (const f of KEEP) writeFileSync(join(def, f), 'precious');
33+
mkdirSync(join(def, 'IndexedDB'), { recursive: true });
34+
writeFileSync(join(def, 'IndexedDB', 'data'), 'precious');
35+
36+
return { home, data };
37+
}
38+
39+
function clean(home: string, args: string[], env: Record<string, string> = {}) {
40+
const result = spawnSync('sh', [INSTALL_SH, 'clean', ...args], {
41+
encoding: 'utf8',
42+
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: home, ...env },
43+
});
44+
if (result.status !== 0) {
45+
throw new Error(`install.sh clean exited ${result.status}\n${result.stderr}\n${result.stdout}`);
46+
}
47+
return `${result.stdout}${result.stderr}`;
48+
}
49+
50+
const cachesExist = (data: string) =>
51+
CACHES.map((c) => existsSync(join(data, 'Default', c)));
52+
53+
afterAll(() => {
54+
for (const r of roots) rmSync(r, { recursive: true, force: true });
55+
});
56+
57+
describe('install.sh clean', () => {
58+
it('clears the regenerable caches', () => {
59+
const { home, data } = profile(2);
60+
const out = clean(home, []);
61+
expect(cachesExist(data)).toEqual([false, false, false]);
62+
expect(out).toMatch(/Freed ~\d+MB/);
63+
});
64+
65+
it('keeps everything the user would miss', () => {
66+
const { home, data } = profile(2);
67+
clean(home, []);
68+
for (const f of [...KEEP, 'IndexedDB']) {
69+
expect(existsSync(join(data, 'Default', f)), `${f} should survive`).toBe(true);
70+
}
71+
});
72+
73+
it('leaves a small profile alone with --if-large', () => {
74+
// The automatic pass on upgrade. Clearing a healthy profile every update
75+
// would just cost everyone a re-download for nothing.
76+
const { home, data } = profile(1);
77+
clean(home, ['--if-large']);
78+
expect(cachesExist(data)).toEqual([true, true, true]);
79+
});
80+
81+
it('clears past the limit with --if-large', () => {
82+
const { home, data } = profile(2);
83+
clean(home, ['--if-large'], { TRONBROWSER_CACHE_LIMIT_MB: '4' });
84+
expect(cachesExist(data)).toEqual([false, false, false]);
85+
});
86+
87+
it('honors TRONBROWSER_CACHE_LIMIT_MB=0 as "never automatically"', () => {
88+
const { home, data } = profile(2);
89+
clean(home, ['--if-large'], { TRONBROWSER_CACHE_LIMIT_MB: '0' });
90+
expect(cachesExist(data)).toEqual([true, true, true]);
91+
});
92+
93+
it('still clears on an explicit clean when the automatic pass is disabled', () => {
94+
const { home, data } = profile(2);
95+
clean(home, [], { TRONBROWSER_CACHE_LIMIT_MB: '0' });
96+
expect(cachesExist(data)).toEqual([false, false, false]);
97+
});
98+
99+
it('does nothing when there is no profile', () => {
100+
const home = mkdtempSync(join(tmpdir(), 'tron-clean-empty-'));
101+
roots.push(home);
102+
expect(() => clean(home, [])).not.toThrow();
103+
});
104+
105+
it('respects TRONBROWSER_DATA', () => {
106+
const { home } = profile(2);
107+
const alt = join(home, 'elsewhere');
108+
mkdirSync(join(alt, 'Default', 'Cache'), { recursive: true });
109+
writeFileSync(join(alt, 'Default', 'Cache', 'blob'), Buffer.alloc(2 * 1024 * 1024, 1));
110+
111+
clean(home, [], { TRONBROWSER_DATA: alt });
112+
expect(existsSync(join(alt, 'Default', 'Cache'))).toBe(false);
113+
// The default location wasn't touched, because it wasn't the one named.
114+
expect(existsSync(join(home, '.tronbrowser', 'Default', 'Cache'))).toBe(true);
115+
});
116+
});

0 commit comments

Comments
 (0)