[macOS] Huge memory usage and crash after trying to open large folder (~30k images)

*** Please report new bugs here! ***

Moderators: xnview, Dreamer

.wojtek
Posts: 9
Joined: Sat Nov 19, 2011 11:08 am

[macOS] Huge memory usage and crash after trying to open large folder (~30k images)

Post by .wojtek »

Version: `1.12.3 [10271] MP` / macOS

When trying to open folder with a lot of images XnView MP starts gobbling up all memory and crashes.

Attached are after-crash thread-dump and then "spindump" and "muestra" from activity monitor.

AI generated analysis:

# Bug report: XnView MP 1.12.3 hangs and consumes extreme memory when browsing a large photo folder

## Summary

After launching XnView MP 1.12.3 on macOS and entering a directory containing a large number of photos, the application's memory footprint grows to tens of gigabytes and the UI becomes unresponsive (beachball) for over a minute. The main thread is not blocked on thumbnail decoding; it is saturated by per-thumbnail view updates on the Qt/Cocoa bridge, combined with extremely slow per-item date/time label formatting through QLocale/ICU. Autoreleased Objective-C objects from these updates accumulate until the autorelease pool is drained, at which point the process footprint has far exceeded physical RAM and the main thread stalls in malloc/deferred-reclamation paths while the system swaps.

## Environment

| Item | Value |
| --- | --- |
| Application | XnView MP 1.12.3 (1.12.3), identifier `com.xnview.XnView` |
| Qt version bundled | Qt 6.8.9 (QtCore/QtWidgets/libqcocoa) |
| OS | macOS 27.2.1 (Build 26B131) |
| Hardware | MacBookPro18,1 (Apple Silicon, arm64), 32 GB RAM, 10 active CPUs |
| Date of capture | 2026-11-15, ~13:36 local |
| Reports attached | `sample` of the live process (1 ms interval, 1219 samples) and a hang `spindump` (stackshots, Report Version 73) |

## Symptoms

1. Memory: physical footprint 29.4 GB in the sample; 50.81 GB -> 62.88 GB during a 10-second spindump sampling window; 108.67 GB footprint at the later hang report. On a 32 GB machine this forces heavy compression and swap.
2. Hangs: process unresponsive for 36 s and, in a second capture, 93 s.
3. Worker threads (`ThumbLoaderThread`, `BitmapLoadThread`, 5 Qt pooled threads) are 100% idle, waiting on `QWaitCondition`. Thumbnail decoding is NOT the bottleneck.

## Evidence: where the main thread spends its time

In the 1 ms sample (1219 samples), 1215 samples of the main thread sit in a single chain:

```
ThumbLoader::thumbLoaded(int)
ThumbLoader::loaded(int)
ItemModel::updateItem(int, bool)
```

That is: every completed thumbnail posts one event to the GUI thread, with no batching. Each event has two cost centers:

### 1. ~84% of main-thread time: dataChanged -> view -> Qt/Cocoa bridge (mass ObjC allocation)

```
ItemModel::updateItem + 272 1024 samples
QAbstractItemModel::dataChanged 1024
QAbstractItemView::dataChanged 1020
??? (libqcocoa.dylib) 1017
_objc_rootAllocWithZone / -[__NSArrayM insertObject:atIndex:] /
objc_msgSendSuper2 / objc_retain / objc_release ...
```

Each single-item update triggers mass Objective-C object creation in the Cocoa widget layer (arrays, allocations, class realization). The hottest leaf functions in the whole process are `objc_msgSend` (216), `-[__NSArrayM insertObject:atIndex:]` (133), `_xzm_malloc_tc` (129), `_xzm_xzone_thread_cache_fill_and_malloc` (105), `_objc_rootAllocWithZone` (66).

### 2. ~16% of main-thread time: label formatting via QLocale/ICU, rebuilt per item

```
ItemModel::updateItem + 64 190 samples
ItemData::updateLabels 188
TemplateGetDateTime 68 (+49, +7 in sibling branches)
QTime::toString -> QLocale::toString
QCalendarBackend::dateTimeToString 67
QTimeZone::displayName 42 (+24)
@objc _NSSwiftTimeZone.abbreviation(for:) 40
LocaleCache._currentAndCache 38
_LocaleICU.init ... (ICU: numbering systems, calendar lookup,
CFBundleCopyPreferredLanguages fallbacks, etc.)
```

Formatting the date/time label of one thumbnail constructs a full ICU locale context (numbering systems, calendar identifier, localization fallbacks from the bundle) from scratch, every time. This confirms that a thumbnail label template containing date/time tokens is active, and that this formatting path is pathologically slow.

### 3. The hang itself: autorelease pool drain under extreme memory pressure

The spindump's heaviest main-thread stack during the unresponsive period is:

```
_CFAutoreleasePoolPop
objc_autoreleasePoolPop
AutoreleasePoolPage::releaseUntil
-[__NSArrayM dealloc] -> __RELEASE_OBJECTS_IN_THE_ARRAY__
??? (libqcocoa.dylib) / _xzm_free_tc
_xzm_xzone_madvise_batch
xzm_reclaim_mark_free_locked
mach_vm_reclaim_ring_resize
mach_vm_deferred_reclamation_buffer_resize
mach_msg2_trap (kernel)
```

The ObjC objects created in (1) are autoreleased, so they accumulate during the whole folder scan and are only released when the pool is popped. By then the footprint is tens of GB beyond physical RAM; draining the pool and returning pages to the OS (madvise / deferred reclamation) takes tens of seconds under swap pressure, the run loop never returns to event processing, and the app beachballs. One kernel frame even shows a rwlock in the VM subsystem blocked on the XnViewMP process itself.

## Root cause (analysis)

Two independent defects compound:

1. **No batching of thumbnail-completed updates.** Each of the (presumably thousands of) thumbnails in the folder posts an individual `ThumbLoader::thumbLoaded` event that runs `ItemModel::updateItem` and a full `dataChanged` round-trip through the view and the Cocoa bridge on the GUI thread. Nothing coalesces these into periodic bulk refreshes.
2. **Per-item allocation churn and per-item locale reconstruction.** Each update allocates large numbers of autoreleased ObjC objects, and `ItemData::updateLabels` re-creates an expensive ICU/QLocale context per item instead of caching the locale/formatter. Combined with the unbounded autorelease window, memory grows without limit until the machine swaps and the GUI thread stalls in memory reclamation.

## Impact

Application becomes unusable (36-93 s hangs, potentially indefinite) and destabilizes the whole system (swap pressure on a 32 GB machine) simply by entering a large photo folder.

## Workarounds for end users

- Disable thumbnail labels, or remove date/time tokens from the label template (Tools -> Settings -> Browser -> Labels). This removes the ICU/QLocale hot path.
- Browse via filters or subfolders instead of one large flat folder.
- (Developers may also note this reproduces regardless of thumbnail cache size; the growth is view-layer objects, not bitmaps.)

## Suggested fixes for the developer

1. **Batch thumbnail updates.** Coalesce `ThumbLoader::thumbLoaded` callbacks: collect completed indices and emit one `dataChanged` (or `QAbstractItemModel::dataChanged` for ranges / `layoutChanged`) every N ms or every N items, instead of one event per thumbnail. This is the single highest-impact change.
2. **Cache the locale and formatter.** In `ItemData::updateLabels` / `TemplateGetDateTime`, cache the `QLocale` (and any `QTimeZone::displayName` result, which is stable for a given zone) instead of letting `QLocale::toString` rebuild ICU context per call. Consider `QLocale::system()` reuse or a static formatter.
3. **Scope the autorelease pool.** Wrap the per-item Cocoa update work in `QMacAutoReleasePool` (or drain periodically), so autoreleased ObjC objects do not accumulate for the entire folder scan.
4. **Cap memory during bulk loading.** Consider pausing thumbnail loading when the pending-update queue exceeds a threshold, or processing updates incrementally with `QTimer::singleShot(0, ...)` yielding to the event loop so the UI stays responsive.
5. **Optional:** audit the Qt/Cocoa bridge path under `QAbstractItemView::dataChanged` (libqcocoa +0x86xxx frames) for per-update NSArray/view-object creation that could be reused across updates.

## Attachments

- `Muestra de XnView.txt`: `sample` output of the live process, 1 ms interval, 1219 samples (complete).
- `xnview.txt` and `Spindump.txt`: hang spindump (Report Version 73), containing the 36 s/93 s unresponsiveness data and the autorelease-pool/mach_vm_reclaim hang stack. (Partial in this upload; full files available on request.)
You do not have the required permissions to view the files attached to this post.