Skip to content

Conversation

@xvrh
Copy link

@xvrh xvrh commented Nov 15, 2023

cc @iosephmagno

Lottie v3.0.0-alpha.1 has an experimental enableRenderCache that should replace any previous attempt (CacheLottie etc..)

@delfme
Copy link
Owner

delfme commented Nov 16, 2023

Thx Xavier, will have a look at it next week and revert 👌

@delfme
Copy link
Owner

delfme commented Nov 24, 2023

Hello @xvrh! Just tried it inside Presence and app crashed with this error.

[ServicesDaemonManager] interruptionHandler is called. -[FontServicesDaemonManager connection]_block_invoke
* thread #77, queue = 'com.Metal.CommandQueueDispatch', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=2098 MB, unused=0x0)
    frame #0: 0x0000000200e80da8 libsystem_kernel.dylib`mach_msg2_trap + 8
libsystem_kernel.dylib`mach_msg2_trap:
->  0x200e80da8 <+8>: ret    
libsystem_kernel.dylib`macx_swapon:
    0x200e80dac <+0>: mov    x16, #-0x30
    0x200e80db0 <+4>: svc    #0x80
    0x200e80db4 <+8>: ret    
Target 0: (Runner) stopped.

To reproduce it:

  1. Add the enableRenderCache: true, to return Lottie()
    https://github.com/presence-app/Presence/blob/f9dd6b3801e6063559dc7b55d3595d2ec70c4fa5/lib/widgets/sticker_packs/sticker_view.dart#L196-L206
  2. Enter Saved Messages chatroom
  3. Open sticker keyboard and load different packs
  4. Keep switching packs selection
    At some point app crashes due to a memory issue.

@xvrh
Copy link
Author

xvrh commented Nov 24, 2023

@delfme I'm not able to run your app on iOS. First I don't have access to your appstoreconnect team so I have to change the bundleId in the projects etc...
Then I hit this xcode error:

Cycle inside Runner; building could produce unreliable results.
Cycle details:
→ Target 'Runner': ExtractAppIntentsMetadata
○ Target 'Runner' has copy command from '/Users/xavier/projects/Presence/ios/NotificationServiceExtension/NotificationService.swift' to '/Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationService.swift'
○ That command depends on command in Target 'Runner': script phase “Thin Binary”
○ Target 'Runner' has process command with output '/Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/Info.plist'
○ Target 'Runner' has copy command from '/Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/NotificationServiceExtension.appex' to '/Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationServiceExtension.appex'


Raw dependency cycle trace:

target:  ->

node: <all> ->

command: <all> ->

node: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/Objects-normal/arm64/ExtractedAppShortcutsMetadata.stringsdata ->

command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:ExtractAppIntentsMetadata ->

node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase5-copy-files> ->

command: P0:::Gate target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase5-copy-files ->

node: <Copy /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationService.swift> ->

command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:Copy /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationService.swift /Users/xavier/projects/Presence/ios/NotificationServiceExtension/NotificationService.swift ->

CYCLE POINT ->

node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase4--cp--embed-pods-frameworks> ->

command: P0:::Gate target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase4--cp--embed-pods-frameworks ->

node: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/InputFileList-444D146ED5D89FA1B5361036-Pods-Runner-frameworks-Debug-input-files-775c8d5932d2d41b6f6e4df8e90d2aa0-resolved.xcfilelist ->

command: P2:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:WriteAuxiliaryFile /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/InputFileList-444D146ED5D89FA1B5361036-Pods-Runner-frameworks-Debug-input-files-775c8d5932d2d41b6f6e4df8e90d2aa0-resolved.xcfilelist ->

node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase3-thin-binary> ->

command: P0:::Gate target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase3-thin-binary ->

node: <execute-shell-script-18c1723432283e0cc55f10a6dcfd9e02f1eee2015e8ff5ebcd27678f788c2826-target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-> ->

command: P2:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:PhaseScriptExecution Thin Binary /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/Script-3B06AD1E1E4923F5004D2608.sh ->

node: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/Info.plist/ ->

directoryTreeSignature: � ->

directoryContents: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/Info.plist ->

node: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/Info.plist ->

command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:ProcessInfoPlistFile /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/Info.plist /Users/xavier/projects/Presence/ios/Runner/Info.plist ->

node: /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationServiceExtension.appex ->

command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:Copy /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/Runner.app/PlugIns/NotificationServiceExtension.appex /Users/xavier/Library/Developer/Xcode/DerivedData/Runner-batnsfluotvrtbfddqajsbttmdha/Build/Products/Debug-iphoneos/NotificationServiceExtension.appex ->

node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase4--cp--embed-pods-frameworks>

I can have a look at your out-of-memory crash, but can you provide a runnable independent example?

@iosephmagno
Copy link

Maybe issue can be reproduced in this sample as well by loading 100 different animated stickers instead of using the same sticker.

Can we add cacheSize: 60 ?
What happens when cacheSize is reached? Is cache cleared entirely or do we use FIFO to evict old lotties and give room to newest one?

@xvrh
Copy link
Author

xvrh commented Nov 25, 2023

@iosephmagno currently the cache is limited to 100MB: https://github.com/xvrh/lottie-flutter/blob/master/lib/src/render_cache.dart#L18. When this limit is reached, no other images are created (until some are released). When an animation disappear from the screen, it should release the memory immediately.

To decide on the 100MB limit, I did my experiment with an iPhone Pro with a simplistic sample app. It seems I was too optimistic. We should probably lower the memory by default (50? 20?) and expose this property publicly so we can adapt this choice depending on the phone model.

I'm also trying to validate that my assumptions are correct and that images are correctly disposed with no leak.

One thing you should absolutely change in your settings, is remove the FrameRate(240) this will create 4x too many images and limit very much the number of frame you can cache.

@xvrh
Copy link
Author

xvrh commented Nov 25, 2023

I released a new alpha version v3.0.0-alpha.3 with an option to configure the memory limit.

ie.

void main() {
  Lottie.renderCacheMaxMemory = 20000000;
}

Can you try with some values for your app and see what works and what doesn't. If nothing works, then we may have a memory leak to find.

@iosephmagno
Copy link

Thx, will test it and revert.

Usually a sticker zip file is 2-3Kb. Idk how lottie fileSize converts into memory, lets me just assume it gets 10 times bigger, so 2-300KB.
In a chat app scenario we have lotties:

  1. inside stickers keyboard where each pack usually comprises 20-25 lotties.
  2. inside chatroom messageList, where users exchange stickers.
    I would say that 10-15MB might be enough for stickers cache

@iosephmagno
Copy link

My doubt is about auto-evicting lotties from cache when they are not visible on the scene.
This is the best strategy in general, but lets consider the flow of a stickers keyboard:

  1. user loads pack A
  2. user loads pack B
  3. user loads again pack A.
    If we evict stickers when not visible, pack B will be re-loaded from scratch at each tap.
    Am I correct?

@xvrh
Copy link
Author

xvrh commented Nov 26, 2023

lets me just assume it gets 10 times bigger

There is now 2 caches at 2 different levels:

  • The old LottieCache which caches the LottieComposition using the url/asset-key as the cache key. This cache ensures that an animation is only loaded and parsed once. This cache keeps 1000 compositions in memory and after that is will evicts the least-recently used. This is implemented here: https://github.com/xvrh/lottie-flutter/blob/master/lib/src/providers/lottie_provider.dart#L24.
    One 1KB lottie file will map to +/- 1KB memory of LottieComposition.

  • The new RenderCache caches the rendered bitmaps. All the frames of an animation rasterised at render size.
    A 10-seconds animation rendered as full screen (on an iphone) with frame rate 60 will take: 1179 x 2556 x 60 x 10 = 1.8GB.
    No need to say, this cache is meant for small and short animations. This is when you have an always visible animation that consume too much battery and you want to trade memory to spare GPU/CPU cycles.

Now I realise this RenderCache may not be useful to you. I can explore yet another intermediate solution. Instead of caching the rasterised images, I can just cache the drawing commands. It will take a lot less memory, save a CPU usage but NOT GPU usage. Maybe this will work for you.

@delfme
Copy link
Owner

delfme commented Nov 26, 2023

lets me just assume it gets 10 times bigger

There is now 2 caches at 2 different levels:

  • The old LottieCache which caches the LottieComposition using the url/asset-key as the cache key. This cache ensures that an animation is only loaded and parsed once. This cache keeps 1000 compositions in memory and after that is will evicts the least-recently used. This is implemented here: https://github.com/xvrh/lottie-flutter/blob/master/lib/src/providers/lottie_provider.dart#L24.
    One 1KB lottie file will map to +/- 1KB memory of LottieComposition.
  • The new RenderCache caches the rendered bitmaps. All the frames of an animation rasterised at render size.
    A 10-seconds animation rendered as full screen (on an iphone) with frame rate 60 will take: 1179 x 2556 x 60 x 10 = 1.8GB.
    No need to say, this cache is meant for small and short animations. This is when you have an always visible animation that consume too much battery and you want to trade memory to spare GPU/CPU cycles.

Now I realise this RenderCache may not be useful to you. I can explore yet another intermediate solution. Instead of caching the rasterised images, I can just cache the drawing commands. It will take a lot less memory, save a CPU usage but NOT GPU usage. Maybe this will work for you.

  1. It seems this cache is enabled by default. Am I correct?
    Also, since it is cached inside a Map, we can avoid to run clear(). I mean, cache will be cleared at every app launch or by the system when it decides it is time to clear memory. Wouldn't it be better to cache compositions inside Hive? I mean, this way they will be available at next app launch.

  2. Now it is clear why app was crashing :)

@delfme
Copy link
Owner

delfme commented Nov 26, 2023

I just realized that we do pre-cache compositions (when stickers are downloaded form server) and then we pass cached compositions to Lottie. So maybe we are not really using plugin own's cache (1).
https://github.com/presence-app/Presence/blob/f9dd6b3801e6063559dc7b55d3595d2ec70c4fa5/lib/providers/stickers_provider.dart#L28-L39

@xvrh
Copy link
Author

xvrh commented Nov 26, 2023

@delfme

It seems this cache is enabled by default.

Yes it is enabled by default. This cache is really the same as what we have with standard Image.network widget or package:flutter_svg.

I mean, cache will be cleared at every app launch or by the system when it decides it is time to clear memory

It's a memory cache, so it's lost when app close. The system limit the cache to 1000 entry, then free the oldest entries. There is a clear method accessible.

Wouldn't it be better to cache compositions inside Hive

You can cache network assets in a persistent cache if you want. But this is not really the point of this cache.

Now it is clear why app was crashing :)

Actually, it's not clear to me. The RenderCache is limited to 100MiB (in 3.0.0-alpha.1) and 50MB (in alpha.3). I expected to not crash with this limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants