v0.23.0 — cached argument specs, reachable worker diagnostics
Per-class argument specs are memoized
extract_argument_specs derives a function class's arguments from data that
cannot change once the class exists — its annotations, its FunctionArguments,
and the Arg descriptors on its MRO — but re-derived them on every catalog
listing and every overload match.
Profiling one run of the upstream integration suite through the Cedar proxy:
58,058 calls costing 15.8s, driving 131,653 typing.get_type_hints calls, 2.4M
_eval_type calls and 648k compile() calls — roughly a quarter of the
worker's CPU.
catalog_schema_contents_functions was 38ms per call where every other
catalog method was 0.1–2ms, entirely because of this. On the same 41,733-RPC
workload after: 16.7ms. The function itself is 179x faster in isolation.
Memoized with functools.cache: thread safe (workers serve on a pool) and
unbounded, because the keys are function classes the catalog already holds for
the process lifetime — it retains nothing that was not already retained. The
cached value is a tuple and the public function returns a fresh list, so a
caller mutating the result cannot corrupt the cache; ArgumentSpec is frozen,
so sharing the specs themselves is safe.
Worker diagnostics reachable without owning the command line
Both of these are what made the profiling above possible at all.
VGI_WORKER_LOG_LEVEL / _FORMAT / _LOGGERS — applied inside
configure_worker_logging rather than in one CLI. The vgi_rpc.access log
already recorded duration_ms, method and row/byte counts for every RPC, but
reaching it required --log-logger and --log-format on the command line,
which anything launching a worker without owning its argv — a container, a
process manager, a test harness — cannot supply. Placed in the shared function
because vgi-serve and the fixture servers had byte-identical logging blocks,
and wiring an override into only one of them would be a trap.
vgi/profiling.py — VGI_WORKER_PROFILE=<dir> writes pstats on exit.
A single profiler, deliberately: under --http a worker runs on waitress, so
a main-thread-only profile reports time in select() and nothing about the work
being measured. Since Python 3.12 cProfile is built on sys.monitoring, whose
tool registration is process-global, so one profiler observes the pool threads
too. The corollary is that there can only be one — a profiler per thread raises
ValueError: Another profiling tool is already active as each waitress thread
starts, which prevents the server from accepting connections at all. SIGTERM
is handled because its default action skips interpreter shutdown, which would
lose the profile on every normal stop.