Repository navigation
[Performance] repeated property spreading causes out of memory error #34599
Copy link
Copy link
Closed
Description
TypeScript Version: 3.7.0-beta
Search Terms: spread out of memory
When strictNullChecks is set, repeatedly spreading properties doesn't scale:
// Slow
const props: Record<string, string> = {
...config.a !== undefined && {a: config.a.toString()},
...config.b !== undefined && {b: config.b.toString()},
...config.c !== undefined && {c: config.c.toString()},
// ...
}// Fast
const props: Record<string, string> = {}
if (config.a !== undefined) props.a = config.a.toString()
if (config.b !== undefined) props.b = config.b.toString()
if (config.c !== undefined) props.c = config.c.toString()
// ...In a real codebase, the issue will likely surface initially as slower and slower compilation times. Eventually, an out of memory error appears to occur randomly since it can be triggered by simply repeating an existing pattern a few more times. The VS Code experience in these scenarios is also poor as the TypeScript server hangs.
Code
interface Props {
readonly a?: string
readonly b?: string
readonly c?: string
readonly d?: string
readonly e?: string
readonly f?: string
readonly g?: string
readonly h?: string
readonly i?: string
readonly j?: string
readonly k?: string
readonly l?: string
readonly m?: string
readonly n?: string
readonly o?: string
readonly p?: string
readonly q?: string
readonly r?: string
readonly s?: string
readonly t?: string
readonly u?: string
readonly v?: string
readonly w?: string
readonly x?: string
readonly y?: string
readonly z?: string
}
function parseWithSpread(config: Record<string, number>): Props {
return {
...config.a !== undefined && {a: config.a.toString()},
...config.b !== undefined && {b: config.b.toString()},
...config.c !== undefined && {c: config.c.toString()},
...config.d !== undefined && {d: config.d.toString()},
...config.e !== undefined && {e: config.e.toString()},
...config.f !== undefined && {f: config.f.toString()},
...config.g !== undefined && {g: config.g.toString()},
...config.h !== undefined && {h: config.h.toString()},
...config.i !== undefined && {i: config.i.toString()},
...config.j !== undefined && {j: config.j.toString()},
...config.k !== undefined && {k: config.k.toString()},
...config.l !== undefined && {l: config.l.toString()},
...config.m !== undefined && {m: config.m.toString()},
...config.n !== undefined && {n: config.n.toString()},
...config.o !== undefined && {o: config.o.toString()},
...config.p !== undefined && {p: config.p.toString()},
...config.q !== undefined && {q: config.q.toString()},
...config.r !== undefined && {r: config.r.toString()},
...config.s !== undefined && {s: config.s.toString()},
...config.t !== undefined && {t: config.t.toString()},
...config.u !== undefined && {u: config.u.toString()},
...config.v !== undefined && {v: config.v.toString()},
...config.w !== undefined && {w: config.w.toString()},
...config.x !== undefined && {x: config.x.toString()},
...config.y !== undefined && {y: config.y.toString()},
...config.z !== undefined && {z: config.z.toString()}
}
}
parseWithSpread({a: 1, b: 2, z: 26})Expected behavior: Compilation performance is on par with conditional syntax.
Actual behavior: An out of memory error occurs.
Related Issues:
Trace:
Local execution of `time tsc --strictNullChecks` (v3.7.0-beta) with the
uncommented code:
<--- Last few GCs --->
at[10896:0x2b128d0] 127908 ms: Mark-sweep 2043.7 (2057.8) -> 2040.9 (2058.1) MB, 752.5 / 0.0 ms (+ 227.7 ms in 67 steps since start of marking, biggest step 17.0 ms, walltime since start of marking 997 ms) (average mu = 0.092, current mu = 0.017) allocat[10896:0x2b128d0] 129003 ms: Mark-sweep 2043.7 (2058.1) -> 2041.6 (2057.6) MB, 917.0 / 0.0 ms (+ 167.1 ms in 43 steps since start of marking, biggest step 10.2 ms, walltime since start of marking 1095 ms) (average mu = 0.048, current mu = 0.011) alloca
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x136cf99]
Security context: 0x1078847808a1 <JSObject>
1: createObjectType(aka createObjectType) [0x35be1a4076d9] [/home/stephen/dev/nature-elsewhere/node_modules/typescript/lib/tsc.js:~29235] [pc=0xeb30e605cfb](this=0x005fc70404a9 <undefined>,16,0x14f39511c269 <Symbol map = 0x234283fb2161>)
2: getSpreadType(aka getSpreadType) [0x174d4996a8d9] [/home/stephen/dev/nature-elsewhere/node_modules/typescript/lib/ts...
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Writing Node.js report to file: report.20191019.123833.10896.0.001.json
Node.js report completed
1: 0x9d2fd0 node::Abort() [node]
2: 0x9d4166 node::OnFatalError(char const*, char const*) [node]
3: 0xb3281e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xb32b99 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xcddeb5 [node]
6: 0xcde546 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
7: 0xcea3da v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
8: 0xceb2e5 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xcedcf8 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [node]
10: 0xcb4627 v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType) [node]
11: 0xfea55b v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
12: 0x136cf99 [node]
Aborted (core dumped)
real 2m10.683s
user 2m50.317s
sys 0m1.371s
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScript
Activity
cjc343 commentedon Oct 22, 2019
Sample react app: https://github.com/cjc343/spread-operator-overflow
Overflows with 21 for me
RyanCavanaugh commentedon Oct 30, 2019
I'm not confident there's going to be a straightforward fix available for this...
weswigham commentedon Oct 30, 2019
Oh boy. I know what's going on and you're not going to like it. So, let's break it down. Start with this:
over in the playground, that'll have a return type of
{}, but in reality before subtype reduction (done during return type fetching) it has a type of{} | { a: string }. Alright, let's add another field:OK, so the first spread is a union of
{} | { a: string }, the second spread is then{} | { b: string }, which we then combine with the first spread - this means for each element of the first union, we combine with each element of the second spread. So we end up with{} | { b: string } | { a: string } | { a: string; b: string }. Alright, neat. Now let's add a 3rd:so let's spread in
{} | { c: string }.... we get{} | { b: string } | { a: string } | { a: string; b: string } | {c: string} | { b: string; c: string; } | { a: string; c: string; } | { a: string; b: string; c: string }. Ah, hopefully by this point, the pattern is apparent - since a union representation is in use, each spread doubles the number of union elements under consideration. 2^25 is something like 33 million, so, naturally, we OOM at that point. The "fast" method is fast because it's less precise - at no point is a union of alternatives ever under consideration, really.niedzielski commentedon Oct 31, 2019
Thank you each for your considerations.
Given the conclusion that the spreads are modeled as:
Is there a more general (and hopefully performant) representation that could be used instead? I think this is saying "it is either present or missing but not undefined" so maybe #13195?
mieszko4 commentedon Jan 16, 2020
I still have an issue with this on
3.8.0-betaso fix in #34853 did not solve the problem for me.Consider the following file
test.jsI get out of memory error when running
ts test.js --allowJsAny suggestion how I could overcome this problem?
weswigham commentedon Jan 16, 2020
We very narrowly optimized the repeaded spread of
undefined | {p1: T}case; a repeated spread of{p1: T} | {h1: U}creates a exponential expansion in the number of types required to compute the expression. In effect, each line you've written there doubles the number of possible output types, and 2^20 is not a small number, by any means.AnyhowStep commentedon Jan 17, 2020
2^20 is tiny compared to Tree(3), though
Runs away
mieszko4 commentedon Jan 17, 2020
Wesley Wigham (@weswigham) thank you for the explanation.
I've noticed that for the example I gave if I replace
...withObject.assigntypescript works well.So maybe that could be a hint how the case with
...could be fixed in the core.jorisw commentedon Aug 31, 2020
I still encounter the memory leak with TypeScript
3.9.6when using the spread operator to compose ThemeUI styles:Each of the objects being spread above are also typed as
SxStyleProps.dotansimha commentedon Aug 31, 2020
Wesley Wigham (@weswigham) I just encountered this with TS 4.0.2. Has a file with ~50 spreads (🤦 ).
If it's done this way:
If I set
somethingorsomethingElseto it's real value (boolean) it fails with an invalid memory allocation in V8 (running with 12GB of RAM...).Also, if I change this to:
It fails :(
I guess the only solution here is to use
Object.assigninstead of spreads?