proj/portage:master commit in: src/, lib/portage/dep/
"Matt Turner" <[email protected]>
| Newsgroups | gmane.linux.gentoo.cvs |
|---|---|
| Message-ID | <1785963928.9a341b0d72f9f1d8a4509feb03f9266a66dc2979.mattst88@gentoo> |
commit: 9a341b0d72f9f1d8a4509feb03f9266a66dc2979
Author: Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug 2 20:17:12 2026 +0000
Commit: Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug 5 21:05:28 2026 +0000
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=9a341b0d
dep: wire C parser into use_reduce fast path
When dep_parser is available, use_reduce() calls dep_parser.parse()
instead of the Python recursive-descent path for the common case (no
per-flag callbacks, EAPI 5+). Falls back to the pure-Python path on
import failure or unsupported options. Parse errors raise
InvalidDependString.
The fast path constructs portage.dep.Atom directly, so the guard
requires token_class to be exactly Atom rather than merely non-None; a
caller asking for some other token class gets the Python path, which
honors it.
The C scanner implements the modern (EAPI 5+) atom grammar
unconditionally -- slot operators, sub-slots, use-dep defaults -- so the
guard must check the EAPI, or under an older EAPI the fast path would
accept atoms that the pure-Python path correctly rejects (e.g.
"dev-libs/foo:=" or "dev-libs/foo[bar]" under EAPI 0/1/4). Only take
the C path when that grammar is valid for the EAPI: eapi is None (an
already-validated, permissive context) or
_get_eapi_attrs(eapi).slot_operator (EAPI 5+). slot_operator is the
newest atom-syntax feature, so it implies use_deps, slot_deps and
sub_slots are enabled too. Older-EAPI strings fall through to the
Python path, which validates them.
Benchmarked over the 80065 dep strings in the ::gentoo md5-cache
(token_class=Atom, matchall=True, eapi=8), best of three, -O2. The
pure-Python path is memoized, so its lru_cache is cleared between runs:
_parser.parse() alone 0.149 s 538806 str/s
use_reduce, Python path 2.913 s 27488 str/s
use_reduce, C path 1.014 s 78938 str/s (2.9x)
The remaining gap to the parse-only baseline is Python-side Atom object
allocation in _c_atom_from_c: 2.76 s above baseline for the Python path
against 0.87 s for the C path, so 3.2x on the object building alone.
Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>
lib/portage/dep/__init__.py | 97 +++++++++++++++++++++++++++++++++++++++++++++
src/dep_parser.c | 6 ++-
2 files changed, 102 insertions(+), 1 deletion(-)
diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index 613ad0b71..9dc2fc8ce 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -52,9 +52,85 @@ from portage.versions import (
ververify,
)
+try:
+ # Not "from . import _parser": pylint reports import-self for that when the
+ # extension has not been built, as in the lint-only CI job.
+ import portage.dep._parser as _c_dep_parser
+except ImportError:
+ _c_dep_parser = None
+
if TYPE_CHECKING:
from _emerge.Package import Package
+
+def _c_atom_from_c(catom, eapi, eapi_attrs, uselist, matchall):
+ """Construct a portage.dep.Atom from a _parser.Atom, bypassing __init__ regex."""
+ a = Atom.__new__(Atom)
+ a._string = str(catom)
+ a._cp = catom.cp
+ a._repo = None
+ a._slot = catom.slot
+ a._sub_slot = catom.sub_slot
+ a._slot_operator = catom.slot_operator
+ ver = catom.version
+ op = catom.operator
+ if op == "=" and ver is not None and ver.endswith("*"):
+ op = "=*"
+ ver = ver[:-1]
+ a._operator = op
+ a._version = ver
+ a._cpv = catom.cpv[:-1] if op == "=*" else catom.cpv
+ a._eapi = eapi
+ a._extended_syntax = False
+ a._build_id = None
+ a._unevaluated_atom = a
+ a._orig_atom = None
+ blocker_str = catom.blocker
+ a._blocker_obj = (
+ Atom._blocker(forbid_overlap=blocker_str == "!!")
+ if blocker_str is not None
+ else None
+ )
+ use_tokens = catom.use
+ if use_tokens is not None:
+ en, dis, miss_en, miss_dis, cond, req = _c_dep_parser.classify_use_deps(
+ use_tokens
+ )
+ a._use = _use_dep(
+ use_tokens,
+ eapi_attrs,
+ enabled_flags=en,
+ disabled_flags=dis,
+ missing_enabled=miss_en,
+ missing_disabled=miss_dis,
+ conditional=cond,
+ required=req,
+ )
+ if not matchall and a._use.conditional:
+ a = a.evaluate_conditionals(uselist)
+ else:
+ a._use = None
+ return a
+
+
+def _c_convert_result(items, eapi, eapi_attrs, uselist, matchall):
+ result = []
+ for item in items:
+ if isinstance(item, str):
+ result.append(item)
+ elif isinstance(item, list):
+ result.append(_c_convert_result(item, eapi, eapi_attrs, uselist, matchall))
+ else:
+ result.append(_c_atom_from_c(item, eapi, eapi_attrs, uselist, matchall))
+ return result
+
+
+def _c_fast_use_reduce(depstr, uselist, matchall, eapi):
+ raw = _c_dep_parser.parse(depstr, uselist=uselist, matchall=matchall)
+ eapi_attrs = _get_eapi_attrs(eapi)
+ return _c_convert_result(raw, eapi, eapi_attrs, uselist, matchall)
+
+
# \w is [a-zA-Z0-9_]
# PMS 3.1.3: A slot name may contain any of the characters [A-Za-z0-9+_.-].
@@ -950,6 +1026,27 @@ def use_reduce(
if subset is not None:
subset = frozenset(subset)
+ if (
+ _c_dep_parser is not None
+ # The fast path builds portage.dep.Atom directly, so it cannot serve
+ # a caller that asked for some other token class.
+ and token_class is Atom
+ and not is_src_uri
+ and not opconvert
+ and not flat
+ and is_valid_flag is None
+ and subset is None
+ and not matchnone
+ and not masklist
+ and not excludeall
+ # C grammar is EAPI 5+ only; eapi=None is permissive.
+ and (eapi is None or _get_eapi_attrs(eapi).slot_operator)
+ ):
+ try:
+ return _c_fast_use_reduce(depstr, uselist, matchall, eapi)
+ except ValueError as e:
+ raise InvalidDependString(str(e)) from e
+
result = _use_reduce_cached(
depstr,
uselist,
diff --git a/src/dep_parser.c b/src/dep_parser.c
index 94dd3fdf2..8940f83b3 100644
--- a/src/dep_parser.c
+++ b/src/dep_parser.c
@@ -627,7 +627,11 @@ py_parse(UNUSED PyObject *self, PyObject *args, PyObject *kwargs)
AUTO_PY useset = NULL;
if (py_uselist != Py_None) {
- useset = PyFrozenSet_New(py_uselist);
+ /* use_reduce() has already frozen its uselist, so the common case is
+ * a frozenset that can be reused instead of copied. */
+ useset = PyFrozenSet_CheckExact(py_uselist)
+ ? Py_NewRef(py_uselist)
+ : PyFrozenSet_New(py_uselist);
if (!useset) {
return NULL;
}