Skip to content

Commit e5ce595

Browse files
authored
perf: optimize the walk() function #587
I noticed that it takes pynvim about 4ms to attach to an nvim instance for me, and 3ms of that is due to the single line: metadata = walk(decode_if_bytes, metadata) This commit reduces the walk() time down to 1.5ms, which brings the total attach time down to 2.5ms. This is helpful for me because in my use case I end up connecting to all of the currently-running nvim processes and this starts to take a noticeable amount of time. Unfortunately parallelization does not help here due to the nature of the slowness. walk() is expensive because it does a very large amount of pure-python manipulation, so this commit is just some tweaks to reduce the overheads: - *args and **kw make the function call slow, and we can avoid needing them by pre-packing the args into fn via functools.partial - The comprehensions can be written to directly construct the objects rather than create a generator which is passed to a constructor - The typechecking is microoptimized by calling type() once and unrolling the `type_ in [list, tuple]` check I did notice that in my setup the metadata contains no byte objects, so the entire call is a noop. I'm not sure if that is something that could be relied on or detected, which could be an even bigger speedup.
1 parent 2c6ee7f commit e5ce595

File tree

3 files changed

+14
-10
lines changed

3 files changed

+14
-10
lines changed

pynvim/api/common.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,15 @@ def decode_if_bytes(obj: T, mode: TDecodeMode = True) -> Union[T, str]:
241241
return obj
242242

243243

244-
def walk(fn: Callable[..., Any], obj: Any, *args: Any, **kwargs: Any) -> Any:
245-
"""Recursively walk an object graph applying `fn`/`args` to objects."""
246-
if type(obj) in [list, tuple]:
247-
return list(walk(fn, o, *args) for o in obj)
248-
if type(obj) is dict:
249-
return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in
250-
obj.items())
251-
return fn(obj, *args, **kwargs)
244+
def walk(fn: Callable[[Any], Any], obj: Any) -> Any:
245+
"""Recursively walk an object graph applying `fn` to objects."""
246+
247+
# Note: this function is very hot, so it is worth being careful
248+
# about performance.
249+
type_ = type(obj)
250+
251+
if type_ is list or type_ is tuple:
252+
return [walk(fn, o) for o in obj]
253+
if type_ is dict:
254+
return {walk(fn, k): walk(fn, v) for k, v in obj.items()}
255+
return fn(obj)

pynvim/api/nvim.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def request(self, name: str, *args: Any, **kwargs: Any) -> Any:
209209
decode = kwargs.pop('decode', self._decode)
210210
args = walk(self._to_nvim, args)
211211
res = self._session.request(name, *args, **kwargs)
212-
return walk(self._from_nvim, res, decode=decode)
212+
return walk(partial(self._from_nvim, decode=decode), res)
213213

214214
def next_message(self) -> Any:
215215
"""Block until a message(request or notification) is available.

pynvim/plugin/host.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def _wrap_delayed_function(self, cls, delayed_handlers, name, sync,
112112

113113
def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args):
114114
if decode:
115-
args = walk(decode_if_bytes, args, decode)
115+
args = walk(partial(decode_if_bytes, mode=decode), args)
116116
if nvim_bind is not None:
117117
args.insert(0, nvim_bind)
118118
try:

0 commit comments

Comments
 (0)