Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFTView poor performance as time passes #91

Open
alexdmotoc opened this issue Jul 15, 2024 · 4 comments
Open

FFTView poor performance as time passes #91

alexdmotoc opened this issue Jul 15, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@alexdmotoc
Copy link

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

I observe a decrease in frame rate for the FTTView as time passes. This can be observed in the cookbook example.

I attached a recording of the view performance after about 2 minutes of running. You can see the lag. Any ideas how to fix?

Crash Logs, Screenshots or Other Attachments (if applicable)

Simulator.Screen.Recording.-.iPhone.15.Pro.-.2024-07-15.at.22.58.52.mp4
@alexdmotoc alexdmotoc added the bug Something isn't working label Jul 15, 2024
@mahal
Copy link
Contributor

mahal commented Aug 3, 2024

The bug is fully reproducible. Leaving the App open for some more minutes even leads to a crash. I'll have a quick look into the crash to gather more information.

@mahal
Copy link
Contributor

mahal commented Aug 3, 2024

Findings without looking at the code:

  • View uses 13% CPU on my blue iPhone even when no sound is playing. Partly to AURemoteIO and partly on main thread.
  • View uses roughly 50% CPU when sound is playing: Partly in SwiftUI.AsyncRenderer
  • When sound is playing: Memory footprint constantly increasest at a rate of 100KB per second
  • CPU usage constantly increases, to 100% in a about a minute. On main thread
  • Memory Graph:
    • There's a UICollectionView List with about 1200 Mutable Dictionaries. These dictionaries contain some runtime information (see below)
    • There are more than 3000 CFStrings lingering around, showing a orange icond with a drop. The contain all the same value eyJEQkd [...] Ijp0cnVlfQ==
    • The Mutable Dictionaires are inited by the CFPrefs and by a CGPath (moveto, quadto, lineto, ...)
  • View Hierarchy with clipped content shows a grotesque amount of ZStacks (see attached screenshot)
  • Xcode shows an Optimization Opportunity: "The layer is using dynamic shadows which are expensive to render. If possible try setting shadowPath, or pre-rendering the shadow into an image and putting it under the layer."
  • Pausing and continuing the app with Debugger: Memory clears, CPU usage dropts to 40% and starts increasing again.

It looks like a memory leak or some silly code that creates elements in a tight loop and garbage collector / reference counter can't act quick enough to free the memory again.

Starting the app with attached debugger will not lead to a crash, even after running for more than an hour (on CPU 100%).

Next step: Look into the code to understand where all these dicts come from.

{
    "ALU instruction count" = 24;
    "Branch instruction count" = 0;
    "Compilation time in milliseconds" = "3.817916";
    "Constant calculation phase present" = 0;
    "Constant calculation temporary register count" = 0;
    "Device atomic instruction count" = 0;
    "Device load instruction count" = 0;
    "Device store instruction count" = 0;
    "FP16 instruction count" = 15;
    "FP32 instruction count" = 8;
    FragmentBufferPrefetch =     (
        promoted
    );
    "INT16 instruction count" = 0;
    "INT32 instruction count" = 0;
    "Instruction count" = 34;
    Remarks = "--- !Analysis\nPass:            prologepilog\nName:            StackSize\nFunction:        agc.main\nArgs:\n  - NumStackBytes:   '0'\n  - String:          ' stack bytes in function'\n...\n--- !Analysis\nPass:            asm-printer\nName:            InstructionMix\nFunction:        agc.main\nArgs:\n  - String:          'BasicBlock: '\n  - BasicBlock:      wrapper_exit\n  - String:          \"\\n\"\n  - String:          ''\n  - String:          ': '\n  - INST_:           '34'\n  - String:          \"\\n\"\n...\n--- !Analysis\nPass:            asm-printer\nName:            InstructionCount\nFunction:        agc.main\nArgs:\n  - NumInstructions: '34'\n  - String:          ' instructions in function'\n...\n";
    "Spilled bytes" = 0;
    "Telemetry Statistics" =     {
    };
    "Temporary register count" = 8;
    "Texture reads instruction count" = 0;
    "Texture writes instruction count" = 0;
    "Thread invariant spilled bytes" = 0;
    "Threadgroup atomic instruction count" = 0;
    "Threadgroup load instruction count" = 0;
    "Threadgroup memory" = 32;
    "Threadgroup store instruction count" = 0;
    "Uniform register count" = 8;
    "Wait instruction count" = 4;
}
image

@mahal
Copy link
Contributor

mahal commented Aug 3, 2024

Code analysis:

  • No Strings and dicts are explicitely created. Could it be due to some debugging stuff? will a release build also have the same problems?
  • Fresh FFT results might come too quick
  • .animation of AmplitudeBar and Cap View might interfere with fresh FFT data: the animation is running until the end even when fresh data is already shown
  • .drawingGroup (Metal rendering) is on HStack, it might be better on each bar (because of List View)
  • GeometryReader in a thight loop (ForEach fresh amplitude reading), this is discouraged by Apple. The width and height could be read once (on appear and on change of orientation / layout) and the passed to the other views in @Environment or as a member

@alexdmotoc
Copy link
Author

@mahal appreciate your insight on this! To me it seems SwiftUI rendering is messing something up, as you showed the huge amount of views rendered one on top of another... I bet that's the key area to look in.

In the mean time I just replace the rendering with Charts (iOS 16+) and it works perfect

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Chart(Array(0 ..< barCount), id: \.self) { index in
                    makeBar(
                        index: index,
                        amplitude: amplitude(at: index),
                        viewHeight: geometry.size.height
                    )
                }
                .chartYScale(domain: 0 ... 0.9)
                .chartXAxis(.hidden)
                .chartYAxis(.hidden)
            }
        }
        .onAppear {
            fft.maxAmplitude = self.maxAmplitude
            fft.minAmplitude = self.minAmplitude
            callbacks.update = fft.update(buffer:)
            callbacks.start = fft.start
            callbacks.stop = fft.stop
        }
        .background(backgroundColor)
    }

    private func makeBar(index: Int, amplitude: Float, viewHeight: CGFloat) -> some ChartContent {
        BarMark(
            x: .value("Frequency", String(index)),
            y: .value("Amplitude", amplitude)
        )
        .foregroundStyle(
            linearGradient.in(CGRect(x: 0, y: 0, width: 0, height: viewHeight))
        )
    }

    private func amplitude(at index: Int) -> Float {
        if index < fft.amplitudes.count {
            return fft.amplitudes[index]
        } else {
            return Constants.silence
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants