Skip to content

FFTView poor performance as time passes #91

Open

Description

@alexdmotoc

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

Activity

  1. mahal commented on Aug 3, 2024

    @mahal
    Contributor

    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.

  2. mahal commented on Aug 3, 2024

    @mahal
    Contributor

    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
  3. mahal commented on Aug 3, 2024

    @mahal
    Contributor

    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
  4. alexdmotoc commented on Aug 3, 2024

    @alexdmotoc
    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions