spacr.qt.resource_cleanup

Free what spaCR owns, and nothing else.

Four buttons in Preferences — clear RAM, clear VRAM, clear CPU, check disk space — and the two performance modes that press them for you. This module is what they call.

The rule, and it is a refusal rather than a preference

“Free as many resources as possible” must never reach anything spaCR does not own. This machine runs other people’s work: a segmentation that starts four seconds sooner is not worth somebody’s eight-hour training run, and a tool that decides otherwise on the user’s behalf is not a tool anybody can leave running.

So, concretely, and asserted in tests/qt/test_resource_cleanup.py by reading this source file:

  • No process is ever killed. Not by name, not by “python processes using a lot of memory”, not spaCR’s own children. There is no os.kill, no signal, no subprocess, no Process.terminate(), no QThread.terminate() anywhere in this module or reachable from it. The Qt layer removed QThread.terminate() outright; a stubborn thread is parked by spacr.qt.bridge.drain_thread(), and that is the strongest thing any of this may do.

  • Nothing here needs root, and nothing here touches the operating system’s own memory. No drop_caches, no sysctl, no swapoff. The page cache belongs to the kernel and dropping it would slow down the whole machine, spaCR included — it is not a free win, it is a transfer from everybody to nobody.

  • A run in flight is never disturbed. No queued job is dropped, no worker is cancelled, no thread pool is emptied of work that has not started yet. spacr.qt.bridge.registry() is read here and never cancelled.

What each button can honestly do

clear RAM

Drops spaCR’s own caches: the merged-field LRU (spacr.crops.clear_field_cache() — by far the largest, whole image stacks), the file-format and DB-format caches, the zoomed-animation cache, the icon and preview lru_caches, the filter-kind cache, every live CropThumbnails thumbnail LRU, and Qt’s own QPixmapCache. A process with no live Qt application also runs gc.collect(). The GUI deliberately does not: walking a heap of live Qt wrappers can enter already-retired C++ objects and crash the process. It cannot return memory the allocator has decided to keep, and it says so when the measured RSS does not move.

clear VRAM

torch.cuda.empty_cache(), which hands the CUDA driver back the blocks torch reserved and is no longer using, plus releasing any model reference spaCR itself is holding (MODEL_RELEASERS). It cannot reclaim another process’s VRAM — no process can — and it cannot free a tensor a running spaCR job is still using, which is the point rather than a limitation.

clear CPU

Releases parked worker threads that have since exited (spacr.qt.bridge.prune_parked_threads()), lets Qt’s global pool retire its idle threads, and lowers spaCR’s own library thread counts (torch, OpenCV) to a floor. It retires idle capacity only: work that is queued or running is left exactly where it is.

check disk space

Read-only. shutil.disk_usage over the filesystems the current project actually touches, deduplicated by device so one line is one drive. It frees nothing and never claims to.

Reporting

Every number is measured before and after by the same function, and Reclaim carries both endpoints so a caller can show the subtraction rather than an estimate. A cleanup that freed nothing reports that it freed nothing; there is no reassuring dialog. See Reclaim.summary().

Classes

BudgetSweep

Observable result of applying the user's live-cache policy.

DiskEntry

One filesystem, and what it is holding.

DiskReport

Every drive the current project touches. Read-only, always.

Reclaim

The measured result of one cleanup. Not the intent — the outcome.

Functions

clear_cpu(→ Reclaim)

Retire spaCR's own idle workers and lower its thread counts.

clear_ram(→ Reclaim)

Drop spaCR's own caches, measured by RSS before and after.

clear_vram(→ Reclaim)

Return torch's reserved-but-unused CUDA blocks to the driver.

confirmation_text(→ str)

What action will actually do, in words, before it does it.

confirmation_title(→ str)

The title of action's confirmation dialog.

cuda_reserved(→ Optional[int])

Bytes the CUDA caching allocator holds for this process, or None.

disk_report(→ DiskReport)

Free space on every drive the project touches. Reads; frees nothing.

human_bytes(→ str)

1536 -> "1.5 KB". Two significant places, never a fake one.

install_budget_sweep(→ bool)

Install the bounded periodic policy sweep on the live QApplication.

install_run_hook(→ bool)

Connect the pre-run cleanup to the run registry. Idempotent.

process_rss(→ int)

This process's resident set size in bytes, or 0 if unknowable.

project_paths(→ List[str])

Folders the current project actually touches, most relevant first.

register(→ bool)

Entry point for spacr.qt.SELF_REGISTERING_MODULES.

run_launch_cleanup(→ List[Reclaim])

The cleanup the mode asks for at launch.

run_pre_run_cleanup(→ List[Reclaim])

The cleanup Extra Performance runs immediately before a module run.

summary_text(→ str)

One paragraph saying what action does.

sweep_memory_budget(→ BudgetSweep)

Apply idle age, one global byte ceiling, and the free-memory floor.

Module Contents

class spacr.qt.resource_cleanup.BudgetSweep[source]

Observable result of applying the user’s live-cache policy.

before_mb/after_mb are sums of the cache owners’ measured entry sizes, not process RSS. That makes the accounting stable and attributable: shared pages and Python’s allocator cannot make an evicted entry appear to have grown. RSS remains the measurement reported by Reclaim for the explicit Clear RAM action.

property freed_mb: float[source]

Measured cache bytes removed by this sweep, in MiB.

class spacr.qt.resource_cleanup.DiskEntry[source]

One filesystem, and what it is holding.

Parameters:
  • path – the folder that was measured; it stands for the whole filesystem it lives on.

  • total – the filesystem’s size, in bytes.

  • used – bytes in use.

  • free – bytes free.

summary() → str[source]

This volume’s free and total space, in human units.

Returns:

a one-line summary.

property percent_used: float[source]

How full this volume is.

Zero for a volume reporting no total, rather than a division error: some network mounts do that, and a resource panel should say nothing rather than fail to draw.

Returns:

the percentage used.

class spacr.qt.resource_cleanup.DiskReport[source]

Every drive the current project touches. Read-only, always.

summary() → str[source]

Every volume, or why there are none to report.

SAYS WHY WHEN EMPTY. “No project folder is known yet” is a different situation from a disk check that found nothing, and a blank panel cannot tell them apart.

Returns:

a one-line summary.

property tightest: DiskEntry | None[source]

The drive with the least room, which is the one worth reading.

class spacr.qt.resource_cleanup.Reclaim[source]

The measured result of one cleanup. Not the intent — the outcome.

Parameters:
  • action – one of ACTIONS.

  • before – the measurement taken before anything ran, in bytes.

  • after – the same measurement, taken after.

  • details – what was actually done, one short phrase each.

  • note – the honest caveat, if there is one. A cleanup that could do nothing in the current state says so here.

  • measured – False when there was no measurement to take at all (no CUDA device, no way to read RSS), which is different from a measurement that came back zero.

summary() → str[source]

One line a dialog can show, and it is allowed to be bad news.

property freed: int[source]

Bytes actually returned, measured. Never negative — memory that grew across the call is reported as zero freed and named in summary(), because “freed -4 MB” is not a thing.

property grew: int[source]

Bytes the measurement went UP by, if it did.

spacr.qt.resource_cleanup.clear_cpu(*, target_threads: int | None = None) → Reclaim[source]

Retire spaCR’s own idle workers and lower its thread counts.

Reads spacr.qt.bridge.registry() to say what is still running; it never cancels it. A parked thread — one that would not stop when its owner went away — is released here only once it has actually exited; spacr.qt.bridge.prune_parked_threads() is the whole mechanism and it waits rather than terminating, because QThread.terminate() on a thread running Python leaves either a held GIL or a corrupt heap.

spacr.qt.resource_cleanup.clear_ram(*, aggressive: bool = False) → Reclaim[source]

Drop spaCR’s own caches, measured by RSS before and after.

Parameters:

aggressive – also drop the caches that are expensive to rebuild (thumbnails, icon pixmaps). The mild form keeps them, because a cleanup that costs the next screen a second of redrawing is not a cleanup, it is a stutter with good intentions.

Returns:

a Reclaim. freed is before - after, from process_rss() — not the size of what was dropped, which would be a guess about an allocator nobody here controls.

spacr.qt.resource_cleanup.clear_vram(*, release_models: bool = True) → Reclaim[source]

Return torch’s reserved-but-unused CUDA blocks to the driver.

Parameters:

release_models – also run MODEL_RELEASERS. Set False immediately before a run — see run_pre_run_cleanup() for why releasing a model spaCR is about to reload is a slowdown wearing an optimisation’s clothes.

This cannot reclaim another process’s VRAM. Nothing can: CUDA memory belongs to the context that allocated it, and the only way to take it back would be to kill that process, which this module does not do to anybody. Nor does it touch tensors a running job still holds — empty_cache() frees blocks the allocator is caching, never live ones, which is exactly why it is safe to call while work is in flight.

spacr.qt.resource_cleanup.confirmation_text(action: str) → str[source]

What action will actually do, in words, before it does it.

The long form, for the confirmation the user is asked to agree to. A bulleted list is right there: they are about to authorise it, and the bullets are what they are authorising.

Parameters:

action – one of ACTIONS; any other value raises KeyError.

spacr.qt.resource_cleanup.confirmation_title(action: str) → str[source]

The title of action’s confirmation dialog.

Parameters:

action – one of ACTIONS; any other value raises KeyError.

spacr.qt.resource_cleanup.cuda_reserved() → int | None[source]

Bytes the CUDA caching allocator holds for this process, or None.

None for “there is nothing to ask”: no torch, no CUDA build, no device, or a CUDA context this process has never initialised. Asking would create that context — several hundred MB of VRAM — which is a strange thing for a button called “clear VRAM” to do.

spacr.qt.resource_cleanup.disk_report(paths: Sequence[str] | None = None) → DiskReport[source]

Free space on every drive the project touches. Reads; frees nothing.

Deduplicated by device id, so a project folder and a home directory on the same disk are one line rather than two identical ones — that duplication is what makes a disk readout stop being read.

RUN THIS ON A WORKER. os.stat and shutil.disk_usage are the calls that wake a sleeping automount, and no cache can answer them without inventing the numbers, so the Qt caller hands this whole function to a JobRunner (spacr.qt.preferences._start_disk_report()). There the wait is unbounded on purpose: a worker is allowed to wait for a mount to wake up, and the drive table is complete.

CALLED ON THE GUI THREAD ANYWAY, it will not freeze it. The whole reading is then bounded by _GUI_DISK_BUDGET_S, and a folder that misses the budget is counted in the note exactly like one that could not be read — because within the time the interface had, it could not be. That is a line missing from a report, against an application that has stopped.

spacr.qt.resource_cleanup.human_bytes(count: int) → str[source]

1536 -> "1.5 KB". Two significant places, never a fake one.

spacr.qt.resource_cleanup.install_budget_sweep() → bool[source]

Install the bounded periodic policy sweep on the live QApplication.

spacr.qt.resource_cleanup.install_run_hook() → bool[source]

Connect the pre-run cleanup to the run registry. Idempotent.

Nothing in bridge.py knows this exists: the registry already emits when a job is registered, and a mode that is not Extra Performance turns the slot into a dictionary lookup and a return.

spacr.qt.resource_cleanup.process_rss() → int[source]

This process’s resident set size in bytes, or 0 if unknowable.

psutil when it is installed, /proc/self/statm when it is not. Zero means “could not measure”, and a cleanup that could not measure says so rather than reporting a freed figure it made up.

spacr.qt.resource_cleanup.project_paths() → List[str][source]

Folders the current project actually touches, most relevant first.

The source folders the user last pointed each module at, plus the two places spaCR writes regardless of where the data lives: the home directory (settings, logs, model downloads) and the temp directory. Only paths that exist are returned.

Called on a worker this is exact. Called on the GUI thread it answers from cache for the remembered folders rather than stat-ing them; see _is_a_folder() for the twenty seconds that bought.

spacr.qt.resource_cleanup.register() → bool[source]

Entry point for spacr.qt.SELF_REGISTERING_MODULES.

Installs the pre-run hook and performs the launch cleanup the mode asks for — once per process, so the test suite calling the launch sequence forty times does not collect forty times.

spacr.qt.resource_cleanup.run_launch_cleanup() → List[Reclaim][source]

The cleanup the mode asks for at launch.

  • Extra Performance — everything, models included: nothing is running yet, so nothing can be taken out from under a job.

  • Performance — RAM and VRAM, gently.

  • Balanced — nothing at all. Returns [] without measuring anything, because a “cleanup” that measures is still a pause.

spacr.qt.resource_cleanup.run_pre_run_cleanup(app_key: str = '') → List[Reclaim][source]

The cleanup Extra Performance runs immediately before a module run.

Only Extra Performance does this, and it is deliberately not the same cleanup as at launch:

  • release_models=False. Releasing a model the run is about to reload is a slowdown dressed as an optimisation — the reclaim is temporary, the reload is seconds of disk and PCIe, and the peak memory is the same either way. empty_cache() still runs, because returning reserved but unused blocks is exactly what a run about to allocate wants.

  • It does not run at all while another run is in flight. The caches this drops are the ones a running job is reading, and a cleanup that competes with the work it is supposed to be helping is worse than no cleanup.

  • It does not touch the CPU: lowering thread counts a moment before a run that wants those threads would slow down the very run it precedes.

spacr.qt.resource_cleanup.summary_text(action: str) → str[source]

One paragraph saying what action does.

Parameters:

action – 'ram', 'vram', 'cpu' or 'disk'.

Returns:

the short form for a hover, falling back to the long form so a new action is never left with no help at all.

spacr.qt.resource_cleanup.sweep_memory_budget(*, now: float | None = None, idle_minutes: float | None = None, ceiling_mb: float | None = None, headroom_short: bool | None = None, max_entries: int = BUDGET_SWEEP_MAX_ENTRIES, owners=None) → BudgetSweep[source]

Apply idle age, one global byte ceiling, and the free-memory floor.

Parameters:
  • now – epoch seconds; explicit so a controlled-clock test can drive real cache entries.

  • idle_minutes – override for tests; otherwise the live preference.

  • ceiling_mb – global RAM-cache ceiling; otherwise the preference.

  • headroom_short – controlled pressure state for tests. When omitted, spacr.qt.memory_budget.headroom_is_short() is measured before and after each pressure eviction, stopping as soon as the floor is restored.

  • max_entries – hard bound on evictions in this call.

  • owners – optional owner sequence for an isolated test; production discovers all already-loaded registered caches.

Returns:

measured, attributable accounting for the pass.

In-use entries are excluded before either policy is evaluated. Their bytes still count against the one process-wide ceiling, so a pinned 100 MB Figure leaves 100 MB less room for evictable thumbnails; applying a full ceiling independently to every cache would multiply the user’s setting by the number of open screens.

Nested helpers

_collect_budget_entries._drop(dropper=dropper, key=key)

Drop this entry’s cached value. Key bound as a default argument.

Bound rather than closed over: closing over the loop variable would give every entry the LAST key, so one eviction would drop the wrong thing and report success.

spacr/qt/resource_cleanup.py:507

_readings_within_the_budget.measure(path: str) → None

Read one folder on this helper thread and record the answer.

Parameters:

path – the folder to measure. Nothing is returned: the answer goes into answers, which the waiting thread reads.

spacr/qt/resource_cleanup.py:1267

project_paths._add(value, *, remembered: bool = True) → None

Add one path, ignoring blanks and duplicates.

remembered is what makes a path dangerous: it means the user chose it and it can therefore be on a sleeping mount. The home and temp directories are not remembered – spaCR reads them from the environment and every start-up has already stat-ed them – so they keep the direct check and are never missing from a first report.

spacr/qt/resource_cleanup.py:1175

sweep_memory_budget._evict(row: _BudgetEntry) → bool

Evict one entry, counting what it freed. Failures are tolerated.

A cache that refuses to drop is not a reason to abandon the sweep – the rest of the budget still needs reclaiming.

spacr/qt/resource_cleanup.py:686