MPXJ version: 16.7.0 (regression — 16.6.0 does not crash on the same file)
Java version: Microsoft Build of OpenJDK 21.0.10 (21.0.10+7-LTS)
OS: Reproduced on both Windows and Linux
Entry point: UniversalProjectReader.read(path) on a .mpp file (MPP14 format), ~1.45 MB
Summary
Reading a specific .mpp file with MPXJ 16.7.0 causes an OutOfMemoryError inside TimephasedDataFactory.getCompleteWork() / splitItem(), regardless of heap size (reproduced with -Xmx512m, -Xmx2048m, and -Xmx6144m — all fail the same way, during a single call to MPP14Reader.processAssignmentData). The file is not large (~1.4 MB), which suggests the allocation is effectively unbounded rather than merely large.
Downgrading only the MPXJ dependency to 16.6.0 (no other code changes) reads the same file successfully, but exposes the underlying cause: several ResourceAssignments in this file contain a raw TimephasedItem where start is after finish (an inverted/invalid time window). MPXJ 16.6.0 rejects these with a clean IllegalArgumentException; MPXJ 16.7.0 appears to loop/recurse without terminating when it encounters the same data.
The 16.7.0 changelog lists exactly one change in this area for that release:
"When reading MPP files, improve handling of timephased data items with durations of less than one hour."
The invalid items found (see table below) mostly have short durations (some under 1 hour) or, in one case, a negative totalAmount, which lines up with that changelog entry. My working hypothesis is that the updated short-duration handling in splitItem doesn't validate start < finish before splitting, and enters a loop that never converges when fed one of these inverted items — hence the unbounded ArrayList growth in the stack trace below. I have not stepped through the 16.7.0 source with a debugger to confirm this directly; flagging it as the most likely mechanism based on the stack trace + changelog + version-diff behavior.
Stack trace (MPXJ 16.7.0)
java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3513)
at java.base/java.util.Arrays.copyOf(Arrays.java:3482)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:483)
at java.base/java.util.ArrayList.add(ArrayList.java:496)
at org.mpxj.mpp.TimephasedDataFactory.splitItem(TimephasedDataFactory.java:300)
at org.mpxj.mpp.TimephasedDataFactory.getCompleteWork(TimephasedDataFactory.java:164)
at org.mpxj.mpp.ResourceAssignmentFactory.process(ResourceAssignmentFactory.java:211)
at org.mpxj.mpp.MPP14Reader.processAssignmentData(MPP14Reader.java:1731)
at org.mpxj.mpp.MPP14Reader.process(MPP14Reader.java:101)
at org.mpxj.mpp.MPPReader.read(MPPReader.java:164)
at org.mpxj.reader.UniversalProjectReader$2.read(UniversalProjectReader.java:613)
at org.mpxj.reader.UniversalProjectReader.read(UniversalProjectReader.java:252)
at org.mpxj.reader.UniversalProjectReader.read(UniversalProjectReader.java:240)
(remaining frames are my application's own call stack into UniversalProjectReader.read(), omitted as not relevant)
Invalid TimephasedItem instances found (via MPXJ 16.6.0's clean rejection)
Recovered by downgrading to 16.6.0 and logging the IllegalArgumentException: Item start must be before item end messages it throws from ResourceAssignment.getTimephasedWork/getTimephasedActualWork/getTimephasedCost/getTimephasedActualCost:
start |
finish |
totalAmount |
amountPerHour |
| 2026-01-05T18:00 |
2026-01-05T15:00 |
-24.615 (negative) |
8.205 |
| 2026-02-24T09:00 |
2026-02-23T18:00 |
5.334 |
5.334 |
| 2026-02-02T10:45 |
2026-01-29T17:15 |
0.019 |
0.076 |
| 2026-02-06T09:00 |
2026-02-05T18:00 |
45.0 |
60.0 |
| 2026-02-06T09:00 |
2026-02-05T18:00 |
45.0 |
60.0 (identical to previous row — likely a linked/copied assignment) |
In every case start is later than finish, never the reverse — this looks systematic rather than random corruption, though I don't know the write path in MS Project that produces it.
Additional finding: not isolated to a specific time window
I tried requesting getTimephasedWork/getTimephasedCost etc. one narrow range at a time (instead of one call covering the whole assignment) to see if only a subset of ranges would fail. Result: 100% of the ranges failed with the identical exception for the affected assignments. This indicates the exception is thrown while MPXJ builds the assignment's complete internal timephased representation from the raw MPP data — before any range-based filtering happens — so no client-side workaround (requesting smaller windows) can avoid it.
Reproduction
I can't share the original file (customer project data). Comparing behavior between 16.6.0 and 16.7.0 on the same file is fully reliable evidence of the regression, but if it helps, I'm happy to try to build and share a minimal synthetic .mpp reproducing an inverted-time TimephasedItem if someone can point me to how such an item could be produced (a specific manual work-split edit near a day boundary in MS Project, resource leveling, etc.) — I haven't yet found a reliable way to construct one from scratch.
Suggested fix direction
TimephasedDataFactory.splitItem/getCompleteWork should validate start < finish (and non-negative amount) before attempting to split an item, and reject/skip invalid items the way 16.6.0's higher-level getTimephasedWork/getTimephasedCost methods already do via a controlled exception — rather than (apparently) looping/recursing without terminating.
MPXJ version: 16.7.0 (regression — 16.6.0 does not crash on the same file)
Java version: Microsoft Build of OpenJDK 21.0.10 (21.0.10+7-LTS)
OS: Reproduced on both Windows and Linux
Entry point:
UniversalProjectReader.read(path)on a.mppfile (MPP14 format), ~1.45 MBSummary
Reading a specific
.mppfile with MPXJ 16.7.0 causes anOutOfMemoryErrorinsideTimephasedDataFactory.getCompleteWork()/splitItem(), regardless of heap size (reproduced with-Xmx512m,-Xmx2048m, and-Xmx6144m— all fail the same way, during a single call toMPP14Reader.processAssignmentData). The file is not large (~1.4 MB), which suggests the allocation is effectively unbounded rather than merely large.Downgrading only the MPXJ dependency to 16.6.0 (no other code changes) reads the same file successfully, but exposes the underlying cause: several
ResourceAssignments in this file contain a rawTimephasedItemwherestartis afterfinish(an inverted/invalid time window). MPXJ 16.6.0 rejects these with a cleanIllegalArgumentException; MPXJ 16.7.0 appears to loop/recurse without terminating when it encounters the same data.The 16.7.0 changelog lists exactly one change in this area for that release:
The invalid items found (see table below) mostly have short durations (some under 1 hour) or, in one case, a negative
totalAmount, which lines up with that changelog entry. My working hypothesis is that the updated short-duration handling insplitItemdoesn't validatestart < finishbefore splitting, and enters a loop that never converges when fed one of these inverted items — hence the unboundedArrayListgrowth in the stack trace below. I have not stepped through the 16.7.0 source with a debugger to confirm this directly; flagging it as the most likely mechanism based on the stack trace + changelog + version-diff behavior.Stack trace (MPXJ 16.7.0)
(remaining frames are my application's own call stack into
UniversalProjectReader.read(), omitted as not relevant)Invalid TimephasedItem instances found (via MPXJ 16.6.0's clean rejection)
Recovered by downgrading to 16.6.0 and logging the
IllegalArgumentException: Item start must be before item endmessages it throws fromResourceAssignment.getTimephasedWork/getTimephasedActualWork/getTimephasedCost/getTimephasedActualCost:startfinishtotalAmountamountPerHourIn every case
startis later thanfinish, never the reverse — this looks systematic rather than random corruption, though I don't know the write path in MS Project that produces it.Additional finding: not isolated to a specific time window
I tried requesting
getTimephasedWork/getTimephasedCostetc. one narrow range at a time (instead of one call covering the whole assignment) to see if only a subset of ranges would fail. Result: 100% of the ranges failed with the identical exception for the affected assignments. This indicates the exception is thrown while MPXJ builds the assignment's complete internal timephased representation from the raw MPP data — before any range-based filtering happens — so no client-side workaround (requesting smaller windows) can avoid it.Reproduction
I can't share the original file (customer project data). Comparing behavior between 16.6.0 and 16.7.0 on the same file is fully reliable evidence of the regression, but if it helps, I'm happy to try to build and share a minimal synthetic
.mppreproducing an inverted-timeTimephasedItemif someone can point me to how such an item could be produced (a specific manual work-split edit near a day boundary in MS Project, resource leveling, etc.) — I haven't yet found a reliable way to construct one from scratch.Suggested fix direction
TimephasedDataFactory.splitItem/getCompleteWorkshould validatestart < finish(and non-negative amount) before attempting to split an item, and reject/skip invalid items the way 16.6.0's higher-levelgetTimephasedWork/getTimephasedCostmethods already do via a controlled exception — rather than (apparently) looping/recursing without terminating.