Income/Expense report with weekly columns takes ~50 s to open in 5.2.2 (2-3 s in 5.1.3)

SUMMARY

One of my reports has become extremely slow after updating to 5.2.2.

It is an “Income and Expenses” report over my whole data range (about 17 years), with the columns set to weeks. In 5.2.2 it takes about 50 seconds until it shows up. The very same report in 5.1.3 opens in 2-3 seconds. Exporting that report to HTML takes just as long, about 50 seconds.

Other reports are fine. The same report with monthly columns opens in 1 second, and the weekly report opens in 1 second as well if I limit it to the last year. So it only gets slow when the report has many weekly columns.

The report itself is not big: the exported HTML has 31 rows and 871 columns.

STEPS TO REPRODUCE

  1. Create an Income/Expense report over a long date range (here: ~17 years).
  2. Set the column type to “Weeks”.
  3. Open the report (or export it as HTML).

OBSERVED RESULT

~50 seconds until the report appears. One CPU core at 100 % the whole time.

EXPECTED RESULT

Comparable to 5.1.3 (2-3 s), or at least comparable to the monthly report on the same data (1 s).

SOFTWARE/OS VERSIONS

  • KMyMoney 5.2.2 (Flatpak org.kde.kmymoney, Qt 6.10.3, Alkimia 8.2.1) - slow
  • KMyMoney 5.1.3 (Ubuntu 24.04 package, Qt 5.15, Alkimia 8.1.2) - fast
  • Linux 6.8.0, Ubuntu 24.04

NOTE ON THE ANALYSIS BELOW

I tried to narrow this down myself with the help of Claude (an AI assistant), which read the source, ran measurements and profiled the running Flatpak build.

It did not find the actual cause. Several plausible-sounding explanations were produced and then disproved one after another by measurement - including its own first and most confident hypothesis, that the switch from WebEngine to QTextBrowser was to blame.

So please treat the section “SUGGESTED DIRECTION” as guesswork. What I do think is worth your time is the measured data and the list of things that were ruled out, so you do not have to walk down those four dead ends yourselves. If any of it looks wrong, trust your own judgement over it - I cannot verify the reasoning myself.


MEASUREMENTS

Timing depends on the internal column count, not on the number of transactions

Configuration internal columns transactions time
Months, full range ~200 all 1 s
Weeks, 1 year 365 (day grid) 1 year 1 s
Days, 1 year 365 1 year 1-2 s
Weeks, full range ~6100 all 50 s

Note that “Weeks, 1 year” and “Months, full range” have very different transaction counts but the same runtime, while “Weeks, full range” differs from “Months, full range” only in the column count. The cost is essentially linear in the internal column count, at roughly 5-8 ms per column (with only 31 account rows, i.e. on the order of 250 us per grid cell).

Weekly reports build the grid at daily resolution

In PivotTable::init(), m_numColumns is initially the number of days when m_config.isColumnsAreDays() is true (columnValue() returns day differences); collapseColumns() folds 7 day columns into one week column afterwards (kmymoney/plugins/views/reports/core/pivottable.cpp, collapseColumns()).

A weekly report over 17 years therefore works on ~6100 columns instead of ~871 - seven times more than the result needs. Monthly reports do not pay this, because columnValue() computes in months directly. This explains why weekly (and daily) reports are the ones affected.

Profile: the time is spent in money value operations, not in rendering

Poor man’s profiler (gdb, 2254 samples over 180 s, covering both a report open and an HTML export) against the running 5.2.2 Flatpak:

=== innermost frame (function) ===

716 31.8% libalkimia6: AlkValue::AlkValue(AlkValue const&)

705 31.3% libc: sem_trywait (idle)

613 27.2% libalkimia6: AlkValue::~AlkValue()

30 1.3% libQt6Gui (all frames combined)

=== library anywhere in the stack ===

72.3% reportsview.so

60.6% libalkimia6

46.8% libkmm_mymoney

Excluding idle samples, ~86 % of the CPU time is spent constructing and destroying AlkValue objects, called from the reports plugin. Qt’s text layout accounts for 1.3 %.

Caveat: the Flatpak plugin is stripped, so symbols inside reportsview.so could not be resolved; the exact loop could not be identified from the outside. Symbol attribution inside libalkimia6 is to the nearest exported symbol and may be off by one function.

WHAT WAS RULED OUT (each by measurement, not by reading)

  1. The QTextBrowser renderer (5.1.3 used WebEngine, 5.2 uses KMMTextBrowser). A standalone Qt harness rendering report-shaped tables shows QTextDocument layout to be linear in cell count at ~30 us/cell (with the KMyMoney CSS applied): 208000 cells = 6.2 s, 24392 cells (the actual report size) well under 1 s. Additionally, the HTML export - which never touches the renderer - is just as slow, and libQt6Gui is at 1.3 % in the profile.

  2. Missing fixed column widths in the generated HTML. Measured: no effect whatsoever (auto layout vs. explicit widths, identical timings).

  3. collapseColumns() / the day grid folding itself. A standalone reproduction of the nested QMap/QList grid with 31 rows and 6100 day columns folds in 6 ms (15 ms with an extra shared copy held). Not the bottleneck.

  4. Implicit-sharing detach cascade in the chained m_grid[og][ig][row][eActual][column] += access in assignCell(). A standalone reproduction with 20000 splits at 6100 columns runs in 9-20 ms - under both Qt 5.15 and Qt 6.4. Not the bottleneck.

  5. Alkimia. AlkValue’s copy constructor is unchanged between 8.1.2 and 8.2.1 (d(val.d), QSharedDataPointer); the diff of alkvalue.cpp between the two tags contains only the QRegExp → QRegularExpression port and unrelated fixes.

SUGGESTED DIRECTION FOR DEVELOPERS

Two independent things are worth looking at:

  1. Why is a single grid column so expensive? ~5-8 ms per column with 31 rows is far more than the folding and the currency conversion should cost. With symbols available, a profile of PivotTable::init() should show this immediately. The loops that run at day resolution are the transaction loop and collapseColumns(); the loops that run after folding are convertToDeepCurrency(), convertToBaseCurrency() (one price lookup per cell) and calculateTotals().

  2. Does the day grid need to exist at all? For weekly/daily pitches the grid is allocated and processed at daily resolution and only then folded. Assigning values directly to the target column would remove a factor of 7 for weekly reports independently of point 1.

UNRELATED FINDINGS NOTICED WHILE READING

  • ReportAccount::operator= (kmymoney/plugins/views/reports/core/reportaccount.cpp) copies its own members but never calls MyMoneyAccount::operator=, so the base class part (id, name, type, currency) is not copied on assignment.
  • PivotTable::renderHTML() deep-copies the entire grid into a QList<PivotOuterGroup> just to sort the outer groups; the code comments on this itself. Sorting pointers or iterators would avoid it. (Not performance-relevant at the measured sizes, but it is unnecessary work.)