Skip to content

subprocess: Avoid using timeout(1) to impose a timeout on subprocesses

Simon McVittie requested to merge wip/issue88 into main

Based on !628 (merged), please review that first.


  • !628 (merged)

  • subprocess: Avoid using timeout(1) to impose a timeout on subprocesses

    This is more difficult than you might think, because until GLib 2.78 (which is far too new for us to depend on), there was a potential race condition between g_child_watch_source_new()'s use of waitpid(), and a kill() in the same main-context. We don't really want to use g_child_watch_source_new() anyway, because we don't know whether this library is going to be loaded by a process that is blocking SIGCHLD or handling SIGCHLD itself.

    Avoiding timeout(1) can result in a significant speedup on ARM hardware emulating x86 via FEX-Emu, which makes executable startup disproportionately expensive: each new executable needs to re-initialize the interpreter, which is normally fine, but the cost builds up when we run a very large number of very small processes.

    Avoiding timeout(1) is also good for robustness: in the past, Canonical's Steam Snap app has had an AppArmor profile that did not allow arbitrary coreutils commands to be run.

    In this initial implementation, we poll the process every 100ms. This is much slower than blocking (it adds up to 100ms to every subprocess execution, resulting in a slowdown from around 8 to 96 seconds for steam-runtime-system-info on x86), but it can be used as a fallback. It's in this commit to have an opportunity to test the fallback code path. Subsequent commits will speed this up again.

    Helps: #88 (closed)

  • subprocess: Don't poll subprocesses that already exited

    We expect that "most" subprocesses will exit shortly after closing their stdout and stderr, so by the time we have read their output, polling their exit status will probably succeed.

    In practice, this mostly counteracts the performance penalty from the previous commit on x86. On ARM emulating x86 with FEX-Emu, it's still inconveniently slow (time taken > my patience to wait for it).

  • subprocess: Try to wait for subprocess in a thread instead of polling

    The key trick here is that on any halfway modern kernel, we can use waitid() in a background thread with the WNOWAIT flag, which pauses until the subprocess has exited but does not reap it, allowing the main thread to wait for it again, this time without WNOWAIT, causing the subprocess to be reaped.

    This is considerably faster than the previous implementation, around 30% on both x86 and ARM.

    Resolves: #88 (closed)

Edited by Simon McVittie

Merge request reports