It appears the app somethings crash with an "OutOf...
# confetti
m
It appears the app somethings crash with an "OutOfMemory" exception: > java.lang.OutOfMemoryError: Failed to allocate a 24 byte allocation with 1988256 free bytes and 1941KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC.
Copy code
2026-08-12 12:36:27.329 19531-19590 AndroidRuntime          dev.johnoreilly.confetti             E  FATAL EXCEPTION: DefaultDispatcher-worker-5 (Fix with AI)
                                                                                                    Process: dev.johnoreilly.confetti, PID: 19531
                                                                                                    java.lang.OutOfMemoryError: Failed to allocate a 32 byte allocation with 2000544 free bytes and 1953KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC.
                                                                                                        at java.util.ArrayList.iterator(ArrayList.java:1036)
                                                                                                        at java.util.AbstractCollection.addAll(AbstractCollection.java:335)
                                                                                                        at java.util.LinkedHashSet.<init>(LinkedHashSet.java:182)
                                                                                                        at kotlin.collections.CollectionsKt___CollectionsKt.toMutableSet(_Collections.kt:1906)
                                                                                                        at kotlin.collections.CollectionsKt___CollectionsKt.distinct(_Collections.kt:1836)
                                                                                                        at com.apollographql.cache.normalized.api.MaxAgeProviderKt.allImplements(MaxAgeProvider.kt:150)
                                                                                                        at com.apollographql.cache.normalized.api.MaxAgeProviderKt.access$allImplements(MaxAgeProvider.kt:1)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getMaxAge-5sfh64U(MaxAgeProvider.kt:75)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getParentMaxAge-5sfh64U(MaxAgeProvider.kt:101)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getMaxAge-5sfh64U(MaxAgeProvider.kt:97)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getParentMaxAge-5sfh64U(MaxAgeProvider.kt:101)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getMaxAge-5sfh64U(MaxAgeProvider.kt:97)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getParentMaxAge-5sfh64U(MaxAgeProvider.kt:101)
                                                                                                        at com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider.getMaxAge-5sfh64U(MaxAgeProvider.kt:97)
                                                                                                        at com.apollographql.cache.normalized.api.CacheControlCacheResolver.resolveField(CacheResolver.kt:263)
                                                                                                        at com.apollographql.cache.normalized.internal.CacheBatchReader.collectData(CacheBatchReader.kt:154)
                                                                                                        at com.apollographql.cache.normalized.internal.CacheBatchReader$collectData$1.invokeSuspend(Unknown Source:14)
                                                                                                        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
                                                                                                        at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:278)
                                                                                                        at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
                                                                                                        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
                                                                                                        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
                                                                                                        at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
                                                                                                        at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
                                                                                                        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
                                                                                                        Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@859d4d2, Dispatchers.Default]
m
200 MB of RAM? Yea sounds like a lot.
Can you somewhat reproduce it locally @Marcello Galhardo?
Or if there's some activity you were doing, I could do the same and run a few heap dumps to see what's retaining all of that
m
No, it's intermittent and it happens while the app is running randomly, sometimes.
m
Can you describe at a high level what you're doing? What conf, what screens?
m
Nothing in particular, just navigate around the app.
Maybe it is navigating too fast around the app before the requests finish? Pressing the tabs fast, and so on?
m
Maybe, I'll try
That's with the debug version or release version?
m
Good question, debug running on an emulator.
👍 1
I don't think I can reproduce on prod version.
👍 1
m
Haven't been able to reproduce so far on a Pixel 6a + debug. • context.totalAvailableMemoryBytes() = 268M • BitmapCache = ~50M • ApolloMemoryCache ~10M I've navigated to the session details of all sessions of droidcon Berlin and I'm at ~57M heap, which sounds what it should be.
I'll keep an eye on it
m
I asked AI to give it a shot, it seems that it did get a fix in here: github.com/joreilly/Confetti/pull/1801/…/c841c9274fcc… It isn't happening anymore for me so it seems it worked. Initial loading is also MUCH MUCH faster.
It also did a few other GraphQLs optimizations, I'm sending them all as a single CL in here: github.com/joreilly/Confetti/pull/1801 All app queries and navigation seems much faster but then, I'm not a specialist in GraphQL... 🤔
@mbonnin it seems good to me but I don't have much experience with GraphQL. Could you kindly review with your expertise?
b
This removes cache control but I'd much rather understand the root cause 🙂
I think the feature is actually useful, because conferences tend to shuffle the agenda around while the conference is happening, and so it helps to have relatively fresh data showing
m
That is what AI told me:
Copy code
┃   ### Root Cause
 ┃
 ┃   1. @cacheControl directive triggers Apollo TTL resolver:
 ┃       • extra.graphqls configured @cacheControl(maxAge: ...) directives on types (Session, Speaker, etc.).
 ┃       • Apollo compiler generated a non-empty maxAges map, enabling CacheControlCacheResolver during all normalized cache reads.
 ┃   2. Recursive schema coordinate traversal:
 ┃       • For every single field read across hundreds of cached sessions and speakers, Apollo calls CacheControlCacheResolver.resolveField(...) → SchemaCoordinatesMaxAgeProvider.getMaxAge(...).
 ┃       • getMaxAge recursively calls getParentMaxAge and allImplements(...).
 ┃   3. Massive heap allocation storm:
 ┃       • allImplements(...) continuously constructs ArrayList.iterator, LinkedHashSet, and invokes CollectionsKt.distinct(...) to traverse interface hierarchies.
 ┃       • For a full conference graph, this generates millions of short-lived collection objects in seconds, overwhelming the Android GC and causing OutOfMemoryError on 192MB–200MB heap limit devices.
It also said behaviour is not changed because:
Copy code
Confetti already configures maxStale(Duration.INFINITE) in ApolloClientCache and manages cache freshness explicitly via FetchPolicy (CacheFirst vs NetworkOnly) and Interceptor.
@bod does any of that make sense or is it hallucinating?
b
it probably makes sense but of course I need to check 🙂
👍 1
Ok I remember now: we have a custom fetch policy in Confetti, which: • tries the cache • if it's a hit and not stale according to the max age defined in the schema -> emit that • otherwise -> try the network • if the network succeeds -> emit that • otherwise -> emit the cached value, if any, even stale - because stale is better than nothing • if there was no cached value -> emit an error The
maxStale(Duration.INFINITE)
that the assistant thought meant the max ages were ignored doesn't actually mean that it's just a way to still get stale values from the cache and implement that custom logic. (I should have commented!)
m
I see. What do you recommend here? These changes did fix the crashes and make the app stop to junk/lose frames.
I'd also ask you to kindly take a look at this one too, my app keeps reloading every launch so I was trying to fix it: github.com/joreilly/Confetti/pull/1805
b
I would like to investigate the other potential problems reported by the AI, about too many allocations in the cache implementation.
but of course that takes time 🙂
m
Sure, but just to be clear: this is what stopped the crash: github.com/joreilly/Confetti/pull/1801/…/c841c9274fcc…
The other changes made things faster/smoother.
for me, the app crashes almost every first launch on emulator without this. 😞
i suspect it is happening on my phone too, it's just that my phone is handling it better fwiw
i will change that PR to WIP so that it is not merged until you have time to review
b
I'll see if I repro but from what I understand this doesn't happen to all users though 🙂 I'm not trying to minimize what you're seeing by any means - just before we remove that feature entirely I'd like to understand a bit more
m
i don't think it is a matter of happening to all users, i think it's a matter of your device manages to run that (without running out of memory) or not.
my pixel 9 is josing frames during launch.
anyway, a conference app shouldn't expect to run 200m+ even if the device can run imho
b
Yes, it’s a tradeoff. A problem that renders the app unusable to 99% of users must be fixed asap. A problem that makes the app unusable to 1% of users? And everything in between.
> anyway, a conference app shouldn't expect to run 200m+ even if the device can run imho Agreed
m
i don't think i have the data to say the percentage here, i guess that will affect people with low end devices. so if most of our users are people with good devices (which most likely yes), it is probably hidden behind their expensive phones i guess? 😂
anyway, changed to WIP so it is not merged. take ur time.
🙏 1
can i ask you to also review github.com/joreilly/Confetti/pull/1805 if you have the time?
👍 1
b
I'm genuinely happy we have this problem especially if it's reproducible, because it will help improve the library of course 😄 If that takes too much time and we have a (maybe temporary) quick fix to make the app more usable in the meantime, I don't have any problem with that
🙏 1
m
ups, wrong pull rquest. i meant this one: github.com/joreilly/Confetti/pull/1805
👍 1
j
have to admit I'm not up to date fully with this discussion but just to check are we still ok with cache policy change in github.com/joreilly/Confetti/pull/1803/changes ?
👀 1
b
#1803 LGTM
m
@John O'Reilly can u give me/us permission to add reviewers perhaps? that way i can add @bod + @mbonnin when i touch graphQL.
my knowledge there is almost null, so i do need their review to make sure i didn't break everything LOL
j
will do...once I figure out how 🙂
🙏 1
m
FWIW: My emulator takes about 1+ minutes to load the app every single time I rerun the app, and crash every second run. That's why I ended trying to do those optimizations. 😂
j
which emulator are u using....I use emulator quite a bit here when running app and seems pretty quick.
also haven't encountered crashes but maybe not running in to same issues
I'm mostly using this one (so I can also test adaptive UI behavour)
m
I use those two. API 36 and API 26.
My computer is: • Apple M4 Pro • 48 GB • 26.5.2
Here's how it looks BEFORE the optimization PRs:
Here's how it looks AFTER these optimization PRs:
FYI: I have broken the original PR for "OutOfMemory" into four so that if we decided that any of that AI generated code is worth to merge we can merge independently: • github.com/joreilly/Confetti/pull/1801github.com/joreilly/Confetti/pull/1822github.com/joreilly/Confetti/pull/1821github.com/joreilly/Confetti/pull/1820 Since I don't have the GraphQL/Apollo expertise to evaluate this code, I have included both you @bod and @mbonnin as reviewers.
👍 3
b
Haven't managed to repro an OOM for now. On an Android 16 emulator, running the debug app from the IDE. Looking at the JVM heap it stays around 20-30 MB (out of 192 MB max).
I guess it could be interesting to compare heap dumps. When you get the chance, send me one of those 🙂 (you can get one from the Profiler tool of Android Studio, 'Analyze Memory Usage'' and then in 'Past Recordings' you can get the hprof file)
And now I realize this change was included (which was merged by me by mistake) when I made those tests. So I need to test again... Sorry about the noise!
👍 1
j
b
no 😄
👍 1
😄 1
m
FWIW: I still think we should consider this patch and/or a new caching strategy. I don't think the current implementation offers a good UX for mobile devices.
b
[to be clear I'm not saying we shouldn't merge it - I'm only saying it's not ready to be merged yet]
m
Sorry I've been following from the distance but I agree, about the caching strategy. I've been thinking about this for a while. For something like a conference app, the whole data can be cached locally. It's not like a social network where the graph is potentially terabytes. All the sessions info for a conference in our case is probably in the ~100kB range (haven't checked recently)
1
b
Agree with that, I think the main question is about TTLs
m
The only problem with this is... it's a bad case for GraphQL ^^ Because if I had to redo this with the end user in mind, I would probably just download a .sqlite DB lol
And a notification when it needs updating
Do we really need TTL? Versus etags on opening the conference + pull-to-refresh
Maybe not even need a notification, etags should be enough.
But yea, that's not a great use case for GraphQL....
b
Yeah we need a mechanism to ensure relatively fresh data. It can be something else but I think cache control works 😅
m
Our server doesn't support etags yet. But maybe Google CDN magically supports this
BTW does Google CDN support
QUERY
? 👀
👀 1
Because if it does, that'd make a nice demo
QUERY
+ etags = win
According to docs.cloud.google.com/cdn/docs/caching, Cloud CDN doesn't support it yet
sad panda 1
b
Btw we don't have pull to refresh at the moment, right?
m
Not sure, I want to pull the latest change to
main
only when I have time to look into them 😅
👍 1
m
We don't have a pull to refresh.
👍 1
b
All right, I'm back 🙂. I made more tests with the latest in main: just using the app, navigating to all screens, switching conferences, for ~15 minutes. I saw the heap growing until 50MB, and stagnating between 45-50MB (out of 192MB). This on an Android 16 emulator. Still not sure why this is different from what you're seeing (if you still see it) Marcello.