Lldb is slow to resolve local vars

My apologies, the correct settings command is:
settings set target.experimental.swift-create-module-contexts-in-parallel false

After chatting with a few folks, they suggested the core issue is the transitive closure of any Swift modules which depend on Objective-C code with headers.

This particular (very large) project has a main.swift module, which transitively depends on thousands of Objective-C headers, which I assume all have to be parsed by lldb.

I changed main.swift to main.m and then lldb was able to more or less function. I wonder if this is related?

Hi Ben!

Does lldb work reliably when you set a breakpoint not in main.m, but in some deeper Swift code, e.g. within closure? In my case I have main.m and breakpoints work fast in any objc/c code, but they are slow in Swift code.

Nice to run into you again. @beefon. :slight_smile:

In my case, the breakpoint is not in main; it's the default signal handler handling SIGABRT with a break. (I've also seen it on the break -E ObjC and break -E c++ breakpoints.)

I've tried both true and false values for settings set target.experimental.swift-create-module-contexts-in-parallel. With false value, it is 3 seconds faster.

that should address this contention

I can see lldb-rpc-server loads a single CPU core (100%). Disk I/O is zero to 50 kilobytes/sec most of the time, 0 to ~20 IOPS in Activity Monitor. Does this sound like file system contention?

UPD:

Instruments shows that lldb-rpc-server reads only 140Mb of data during 1 minute of breakpoint 'processing time'. Disk activity is low during this time according to Instruments.

UPD:

System Call Trace shows for 16 seconds capture stat takes around 1.9 seconds. I guess if I extrapolate this to 60 seconds, this means ~10 seconds for stat calls.

Same problem here :frowning:

I took some time to measure how lldb scales with a number of targets with debug symbols enabled.

Apparently, the workaround would be to set GCC_GENERATE_DEBUGGING_SYMBOLS to NO for targets that I don't want to debug most of the time. This will somehow limit lldb experience but will keep it under control especially when you incrementally build your app.

I took another look at the sample you posted in https://gist.github.com/beefon/52555d15d49b5eb1a4ad076482785c34. I don't know how I arrived my initial conclusion that this wasn't clang-related, that sample is spending at least 50% in compiling Clang sources. Since you mentioned that you had a bridging header, this means we are compiling the bridging header from source for every dylib in your program. Fundamentally, bridging headers cannot be cached, because they aren't modular, so they have to be recompiled each time. I believe that you might be able to improve the load time significantly by modularizing as much of your Clang dependencies as possible. This can be as easy as writing module.modulemap files (https://clang.llvm.org/docs/Modules.html) for your header files but may involve some reorganization in case you have complicated cyclic dependencies.

@pykaso A "Same here" comment by itself without any specifics doesn't contribute anything useful to the conversation. Did you mean to say that you took a sample and it looks similar, and you also have large bridging headers?

I'm sorry for my previous useless comment. I was just frustrated by staring at the endless loading animation.

We're facing the same issue for our mid-size project with features separated into 10 subprojects. There are no bridging headers in our project.

The debugger will attach quickly, line with the breakpoint is highlighted but variables evaluation takes from tens of seconds to minutes.

Today I removed all Xcode related files, clear the carthage cache, install the last Xcode beta but the issue still remains.

log enable lldb default:

lldb-rpc-server sample

1 Like

Thanks Adrian!

That said, my bridging header is 2 lines long actually:

#import "Application.h"
#import "CrashReportingSchedulerObjC.h"

and Application.h defines a single ObjC class with a single @import of UIKit, and CrashReportingSchedulerObjC.h has protocol + class definitions with two @import-s of Foundation and Crashlytics.

FWIW, I've just replaced a legacy #import <Foundation/Foundation.h> with modern @import Foundation; and improved breakpoint time by 18 seconds, so now it takes 43 seconds, which is better, but its still 43 seconds on non-incremental non-clean build app start.

@Adrian_Prantl Could you please elaborate a bit more for me. What is Clang dependency? If that is ObjC code, then I currently have 3 ObjC files in main target (I've mentioned two above + main.m), and the main target is the only one that has SWIFT_OBJC_BRIDGING_HEADER set. I can try to move that code off the main target to a separate framework, is this what you mean by 'modularizing as much of your Clang dependencies as possible'?

UPD:

I've completely removed bridging header and wiped SWIFT_OBJC_BRIDGING_HEADER build setting, leaving a single non-swift file main.m in my main target, but it is still 43 seconds.

@pykaso Thanks for providing the detailed data. Your sample appears to spend most of its time compiling textual Swift interfaces. That could be because of a bug I recently fixed in dsymutil.

Are you debugging with .dSYM debug info? You can list all loaded .dSYMs using "image list" in the debugger console.
If yes, do any of the loaded .dSYMs contain a *.dSYM/Contents/Resources/Swift/[x86_64|arm64|...]/Swift.swiftinterface file?
If yes, does deleting this file improve the load time?

1 Like

Could you please elaborate a bit more for me. What is Clang dependency?

That is any Clang module or bridging header directly or indirectly imported by a Swift module you are debugging. Objective-C code as in .m files does not count here. You can have as much Clang-compiled code in your project as you want. It's only the interfaces that get imported into Swift that are relevant, because LLDB's integrated Swift compiler needs to import them to make sense of the types in the Swift module you are debugging.

If your bridging header is that simple I'd highly recommend just replacing it with Clang modules. There is no need to import Foundation via a bridging header, you can just write import Foundation in Swift. You are just defeating the Clang's caching mechanism by importing it through the bridging header. Next step is to write a module map for your other header, you can use this as a starting point

module CrashReportingScheduler {
  header "CrashReportingSchedulerObjC.h"
  export *
}

I don't know what your header imports, so you may need to do this through multiple layers of dependencies...

To reiterate, there is no performance problem with importing Objective-C code as long as you are doing it through import ObjCModuleName in Swift and not through a bridging header.

I've completely removed bridging header and wiped SWIFT_OBJC_BRIDGING_HEADER build setting, leaving a single non-swift file main.m in my main target, but it is still 43 seconds.

Can you take another sample? Can you double-check that you aren't also running into the same issue as @pykaso?

Yes, there were circa 100 files with this name. When I deleted these files, the debugger displayed local variables within 10 sec instead of minutes! Thank you so much! :raised_hands:

Thanks for explanation, Andrian. I've set MODULEMAP_FILE build setting to a newly created file with contents as you provided, but Xcode doesn't like import CrashReportingScheduler: No such module 'CrashReportingScheduler'. I wonder if anything else must be done in order to use clang modules. I'd rather use them instead of bridging header. I can see MODULEMAP_PRIVATE_FILE , SWIFT_INCLUDE_PATHS, should I point any to my modulemap file?

I've captured 4 samples - one at the lldb pause on breakpoint, 2 later on, and the last one right before Xcode would show up local variables. They are without bridging header and without module map file.

Sorry, github gist fails to attach 4x4mb files.

And regards swiftinterface files, find . -name "*.swiftinterface" returns 0 paths when I search in derived data

The module.modulemap file must be in the same directory as the header files it lists, and the search path to the module.modulemap file must be listed via -I (or -Xcc -I).

I'm not sure what Xcode setting translates best to that, but I'd suspect something with INCLUDE in the name.

Can you verify that the performance issue caused by the extra Swift.swiftinterface file in the dSYM is fixed in Xcode 11.4 beta 3? In particular, that the new dsymutil will not copy that file into the .dSYM bundle?

For the reference, we use CocoaPods. And it generates modulemap for every module (local dev pod and usual pods). Currently there are 194 module map files in total.

I've removed most of .modulemap files (and MODULEMAP_FILE build settings), leaving only 23 of them. This shortened lldb time to display local variables down to 36 seconds (from 54). This is a good progress for us. Does it mean lldb unwraps all headers when it attempts to resolve vars for the first time?

For the reference, CocoaPods generates the following structure e.g. for AttributedString target:

AttributedString.modulemap:

framework module AttributedString {
  umbrella header "AttributedString-umbrella.h"

  export *
  module * { export * }
}

AttributedString-umbrella.h

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif


FOUNDATION_EXPORT double AttributedStringVersionNumber;
FOUNDATION_EXPORT const unsigned char AttributedStringVersionString[];

AttributedString-prefix.pch: similar to AttributedString-umbrella.h but without FOUNDATION_EXPORT (last 2 lines).

I've started from scratch and modified CocoaPods to add @import UIKit; instead. I also went through all headers being imported in umbrella headers and replaced legacy #import <> calls with @import ;.
This gave no obvious impact in terms of performance though.

Does it mean even if modulemap contains in fact a reference to just @import UIKit;, lldb slows down simply because the number of these module maps is large enough? Because, as I said, when I remove most of modulemap-s, lldb runs faster quite a bit.

For the reference, we use CocoaPods. And it generates modulemap for every module (local dev pod and usual pods). Currently there are 194 module map files in total.

I've removed most of .modulemap files (and MODULEMAP_FILE build settings), leaving only 23 of them.

I'm not sure I understand what you mean here. Why are you removing module map files? They should be desirable, right? To reiterate, you should want all of you Clang dependencies that you import into Swift imported through modules, not bridging headers.

Does it mean lldb unwraps all headers when it attempts to resolve vars for the first time?

I do not understand what you mean with "unwrap" here. What LLDB does is import the Swift module of the context you are stopped at with all its dependencies, the same way the Swift compiler or REPL would do. In fact, it's the same code.

For the reference, CocoaPods generates the following structure e.g. for AttributedString target:

There seems to be a misconception here. For the most part, it does not matter to Clang whether you import a module by saying "@import UIKit" or by saying "#import <UIKit/UIKit.h". Usually both will trigger a modular import, but "@import" will fail if no module map can be found, whereas "#import" will fall back on a textual include. So there is no need for you to replace #import with @import.

That said, and here it gets tricky, if you are inside a bridging header, or PCH, "#import" will always be a textual include. However, instead of rewriting your dependencies to use "@import" so you can keep using a bridging header, you should strive to just make everything a Clang module and import that from Swift and remove the bridging header entirely. Only if this really isn't possible you can start rewriting "#import" to "@import". That should be your last resort, not your first attempt. I hope that clarifies things a bit.

Terms of Service

Privacy Policy

Cookie Policy