Skip to content

Commit bdafa52

Browse files
Fix: slow app launch under the debugger
1 parent 00970b1 commit bdafa52

4 files changed

Lines changed: 183 additions & 87 deletions

File tree

Lines changed: 3 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
from typing import Optional
32

43
import lldb
54

@@ -8,16 +7,13 @@
87
from .util.log import log
98
from .commands import FieldTypeCommand, SymbolByNameCommand, TypeByAddressCommand, GCCollectCommand
109

11-
from .types.summary import kotlin_object_type_summary, kotlin_objc_class_summary
12-
from .types.proxy import KonanProxyTypeProvider, KonanObjcProxyTypeProvider
10+
from .types.summary import kotlin_object_type_summary
11+
from .types.proxy import KonanProxyTypeProvider
1312

1413
from .cache import LLDBCache
1514

1615
os.environ['CLIENT_TYPE'] = 'Xcode'
1716

18-
KONAN_INIT_PREFIX = '_Konan_init_'
19-
KONAN_INIT_MODULE_NAME = '[0-9a-zA-Z_]+'
20-
KONAN_INIT_SUFFIX = '_kexe'
2117

2218
def __lldb_init_module(debugger: lldb.SBDebugger, _):
2319
log(lambda: "init start")
@@ -27,8 +23,6 @@ def __lldb_init_module(debugger: lldb.SBDebugger, _):
2723
register_commands(debugger)
2824
register_hooks(debugger)
2925

30-
configure_objc_types_init(debugger)
31-
3226
log(lambda: "init end")
3327

3428

@@ -38,78 +32,6 @@ def reset_cache():
3832
LLDBCache.reset()
3933

4034

41-
def configure_objc_types_init(debugger: lldb.SBDebugger):
42-
target = debugger.GetDummyTarget()
43-
breakpoint = target.BreakpointCreateByRegex(
44-
"^{}({})({})?$".format(KONAN_INIT_PREFIX, KONAN_INIT_MODULE_NAME, KONAN_INIT_SUFFIX)
45-
)
46-
breakpoint.SetOneShot(True)
47-
breakpoint.SetAutoContinue(True)
48-
breakpoint.SetScriptCallbackFunction('{}.{}'.format(__name__, configure_objc_types_breakpoint.__name__))
49-
50-
51-
def configure_objc_types_breakpoint(frame: lldb.SBFrame, bp_loc: lldb.SBBreakpointLocation, internal_dict):
52-
process = frame.thread.process
53-
target = process.target
54-
55-
symbols = target.FindSymbols('_OBJC_CLASS_RO_$_KotlinBase')
56-
57-
base_class_name: Optional[str] = None
58-
for symbol_context in symbols:
59-
error = lldb.SBError()
60-
name_addr = process.ReadPointerFromMemory(symbol_context.symbol.addr.GetLoadAddress(target) + 6 * 4, error)
61-
# TODO: Log error?
62-
if not error.success:
63-
continue
64-
base_class_name = process.ReadCStringFromMemory(name_addr, 128, error)
65-
# TODO: Log error?
66-
if not error.success:
67-
continue
68-
69-
break
70-
71-
module_name = frame.symbol.name.removeprefix(KONAN_INIT_PREFIX).removesuffix(KONAN_INIT_SUFFIX)
72-
if module_name == "stdlib":
73-
return False
74-
75-
specifiers_to_register = [
76-
lldb.SBTypeNameSpecifier(
77-
'^{}\\.'.format(module_name),
78-
lldb.eMatchTypeRegex,
79-
),
80-
]
81-
82-
if base_class_name is not None:
83-
objc_class_prefix = base_class_name.removesuffix("Base")
84-
specifiers_to_register.append(
85-
lldb.SBTypeNameSpecifier(
86-
'^{}'.format(objc_class_prefix),
87-
lldb.eMatchTypeRegex,
88-
)
89-
)
90-
91-
category = target.debugger.GetCategory(KOTLIN_CATEGORY)
92-
93-
for type_specifier in specifiers_to_register:
94-
category.AddTypeSummary(
95-
type_specifier,
96-
lldb.SBTypeSummary.CreateWithFunctionName(
97-
'{}.{}'.format(__name__, kotlin_objc_class_summary.__name__),
98-
lldb.eTypeOptionHideValue,
99-
)
100-
)
101-
category.AddTypeSynthetic(
102-
type_specifier,
103-
lldb.SBTypeSynthetic.CreateWithClassName(
104-
'{}.{}'.format(__name__, KonanObjcProxyTypeProvider.__name__),
105-
)
106-
)
107-
108-
bp_loc.GetBreakpoint().SetEnabled(False)
109-
110-
return False
111-
112-
11335
def configure_types(debugger: lldb.SBDebugger):
11436
category = debugger.CreateCategory(KOTLIN_CATEGORY)
11537

@@ -154,9 +76,4 @@ def register_hooks(debugger: lldb.SBDebugger):
15476
# Avoid Kotlin/Native runtime
15577
debugger.HandleCommand('settings set target.process.thread.step-avoid-regexp ^::Kotlin_')
15678

157-
hooks_to_register = [
158-
KonanHook,
159-
]
160-
161-
for hook in hooks_to_register:
162-
debugger.HandleCommand('target stop-hook add -P {}.{}'.format(__name__, hook.__name__))
79+
debugger.HandleCommand('target stop-hook add -P {}.{}'.format(__name__, KonanHook.__name__))

LLDBPlugin/touchlab_kotlin_lldb/cache/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Dict, Optional, Set
22

33
import lldb
44

@@ -28,3 +28,8 @@ def __init__(self):
2828
self._array_header_type: Optional[lldb.SBType] = None
2929
self._runtime_type_size: Optional[lldb.value] = None
3030
self._runtime_type_alignment: Optional[lldb.value] = None
31+
# Modules fully handled (registered or ruled out).
32+
self.registered_module_keys: Set[str] = set()
33+
# Kotlin modules with `^<module>\.` registered but ObjC prefix not yet
34+
# readable; key -> attempt count.
35+
self.pending_prefix: Dict[str, int] = {}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import re
2+
from typing import List, Optional
3+
4+
import lldb
5+
6+
from .types.base import KOTLIN_CATEGORY
7+
from .types.summary import kotlin_objc_class_summary
8+
from .types.proxy import KonanObjcProxyTypeProvider
9+
from .cache import LLDBCache
10+
from .util.log import log
11+
12+
# `_Konan_init_MyMod` (framework) or `_Konan_init_MyApp_kexe` (executable); group 1 = module name.
13+
_KONAN_INIT_RE = re.compile(r'^_Konan_init_([0-9a-zA-Z_]+?)(_kexe)?$')
14+
_KOTLIN_RUNTIME_MARKER = 'Kotlin_initRuntimeIfNeeded'
15+
_KOTLIN_BASE_OBJC_SYMBOL = '_OBJC_CLASS_RO_$_KotlinBase'
16+
# `name` pointer offset in the 64-bit ObjC class_ro_t; all current Apple targets are 64-bit.
17+
_OBJC_CLASS_RO_NAME_OFFSET = 6 * 4
18+
# Skipping these by PATH ALONE — before any symbol API realizes their symtab — is the launch-time win.
19+
_SYSTEM_PATH_MARKERS = ('/usr/lib', '/System/')
20+
# Cap ObjC-prefix retries: the read fails until dyld rebases the class_ro_t name pointer.
21+
_MAX_PREFIX_ATTEMPTS = 16
22+
23+
24+
def _module_key(module: lldb.SBModule) -> str:
25+
# Fall back to path so two no-UUID modules don't collide on one key.
26+
return module.GetUUIDString() or module.GetFileSpec().fullpath or ''
27+
28+
29+
def _is_candidate_module(module: lldb.SBModule) -> bool:
30+
directory = module.GetFileSpec().GetDirectory() or ''
31+
return not any(marker in directory for marker in _SYSTEM_PATH_MARKERS)
32+
33+
34+
def _is_kotlin_module(module: lldb.SBModule) -> bool:
35+
return len(module.FindSymbols(_KOTLIN_RUNTIME_MARKER)) > 0
36+
37+
38+
def _kotlin_module_names(module: lldb.SBModule) -> List[str]:
39+
names: List[str] = []
40+
for symbol in module.symbols:
41+
match = _KONAN_INIT_RE.match(symbol.name or '')
42+
if match is None:
43+
continue
44+
module_name = match.group(1)
45+
if module_name == 'stdlib':
46+
continue
47+
names.append(module_name)
48+
return names
49+
50+
51+
def _read_objc_class_prefix(
52+
target: lldb.SBTarget,
53+
process: lldb.SBProcess,
54+
base_symbols: lldb.SBSymbolContextList,
55+
) -> Optional[str]:
56+
for symbol_context in base_symbols:
57+
error = lldb.SBError()
58+
symbol_addr = symbol_context.symbol.addr.GetLoadAddress(target)
59+
name_addr = process.ReadPointerFromMemory(symbol_addr + _OBJC_CLASS_RO_NAME_OFFSET, error)
60+
if not error.success:
61+
continue
62+
base_class_name = process.ReadCStringFromMemory(name_addr, 128, error)
63+
if not error.success or not base_class_name:
64+
continue
65+
# `or None`: an empty prefix would build `^`, matching every type.
66+
return base_class_name.removesuffix('Base') or None
67+
return None
68+
69+
70+
def _register_specifiers(target: lldb.SBTarget, specifiers: List[lldb.SBTypeNameSpecifier]):
71+
category = target.debugger.GetCategory(KOTLIN_CATEGORY)
72+
for type_specifier in specifiers:
73+
category.AddTypeSummary(
74+
type_specifier,
75+
lldb.SBTypeSummary.CreateWithFunctionName(
76+
'{}.{}'.format(kotlin_objc_class_summary.__module__, kotlin_objc_class_summary.__name__),
77+
lldb.eTypeOptionHideValue,
78+
),
79+
)
80+
category.AddTypeSynthetic(
81+
type_specifier,
82+
lldb.SBTypeSynthetic.CreateWithClassName(
83+
'{}.{}'.format(KonanObjcProxyTypeProvider.__module__, KonanObjcProxyTypeProvider.__name__),
84+
),
85+
)
86+
87+
88+
def _try_register_objc_prefix(
89+
target: lldb.SBTarget,
90+
process: lldb.SBProcess,
91+
cache: 'LLDBCache',
92+
module: lldb.SBModule,
93+
key: str,
94+
) -> bool:
95+
"""Register `^<prefix>`. True when handled (registered / no base class / cap hit), False to retry."""
96+
base_symbols = module.FindSymbols(_KOTLIN_BASE_OBJC_SYMBOL)
97+
if not base_symbols:
98+
return True # no exported ObjC base class; module-name formatters suffice
99+
100+
prefix = _read_objc_class_prefix(target, process, base_symbols)
101+
if prefix:
102+
_register_specifiers(target, [lldb.SBTypeNameSpecifier('^{}'.format(prefix), lldb.eMatchTypeRegex)])
103+
log(lambda: 'Registered ObjC prefix ^{} for {}.'.format(prefix, module.GetFileSpec().GetFilename()))
104+
return True
105+
106+
attempts = cache.pending_prefix.get(key, 0) + 1
107+
cache.pending_prefix[key] = attempts
108+
if attempts >= _MAX_PREFIX_ATTEMPTS:
109+
log(lambda: 'Gave up reading ObjC prefix for {} after {} stops; '
110+
'^<prefix> formatting unavailable.'.format(module.GetFileSpec().GetFilename(), attempts))
111+
return True
112+
return False
113+
114+
115+
def _register_kotlin_module(
116+
target: lldb.SBTarget,
117+
process: lldb.SBProcess,
118+
cache: 'LLDBCache',
119+
module: lldb.SBModule,
120+
key: str,
121+
) -> bool:
122+
"""First sight of a Kotlin module: register `^<module>\\.` now, then attempt the ObjC prefix."""
123+
names = _kotlin_module_names(module)
124+
if not names:
125+
log(lambda: 'Kotlin marker present but no module names for {}; skipping.'.format(
126+
module.GetFileSpec().GetFilename()))
127+
return True
128+
_register_specifiers(target, [
129+
lldb.SBTypeNameSpecifier('^{}\\.'.format(name), lldb.eMatchTypeRegex) for name in names
130+
])
131+
return _try_register_objc_prefix(target, process, cache, module, key)
132+
133+
134+
def scan_and_register_modules(execution_context: lldb.SBExecutionContext):
135+
"""Lazily register Kotlin formatters at stop time. Replaces the old global `_Konan_init_*`
136+
regex breakpoint that realized every module's symtab at launch. Side effect only — never
137+
influences whether the process stops."""
138+
target = execution_context.target
139+
if not target.IsValid():
140+
return
141+
process = target.GetProcess()
142+
if not process.IsValid():
143+
return
144+
145+
cache = LLDBCache.instance()
146+
for i in range(target.GetNumModules()):
147+
module = target.GetModuleAtIndex(i)
148+
key = _module_key(module)
149+
150+
if key in cache.registered_module_keys:
151+
continue
152+
153+
if key in cache.pending_prefix: # known Kotlin module, prefix still settling
154+
if _try_register_objc_prefix(target, process, cache, module, key):
155+
cache.pending_prefix.pop(key, None)
156+
cache.registered_module_keys.add(key)
157+
continue
158+
159+
if not _is_candidate_module(module) or not _is_kotlin_module(module):
160+
cache.registered_module_keys.add(key)
161+
continue
162+
163+
if _register_kotlin_module(target, process, cache, module, key):
164+
cache.pending_prefix.pop(key, None)
165+
cache.registered_module_keys.add(key)

LLDBPlugin/touchlab_kotlin_lldb/stepping/KonanHook.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from .KonanStepIn import KonanStepIn
44
from .KonanStepOut import KonanStepOut
55
from .KonanStepOver import KonanStepOver
6+
from ..module_registration import scan_and_register_modules
7+
from ..util.log import log
68

79
KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS = 'KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS'
810
MAX_SIZE_FOR_STOP_REASON = 20
@@ -18,6 +20,13 @@ def __init__(self, target: lldb.SBTarget, extra_args, _):
1820
pass
1921

2022
def handle_stop(self, execution_context: lldb.SBExecutionContext, stream: lldb.SBStream) -> bool:
23+
# Side-effect only: try/except so registration can never alter the
24+
# stop/continue decision below.
25+
try:
26+
scan_and_register_modules(execution_context)
27+
except Exception as e:
28+
log(lambda: 'Kotlin module registration error: {}'.format(e))
29+
2130
is_bridging_functions_skip_enabled = not execution_context.target.GetEnvironment().Get(
2231
KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS
2332
)

0 commit comments

Comments
 (0)