diff options
-rw-r--r-- | build-aux/measurestack/analyze.py | 123 | ||||
-rw-r--r-- | build-aux/measurestack/app_plugins.py | 158 | ||||
-rw-r--r-- | build-aux/measurestack/util.py | 12 |
3 files changed, 204 insertions, 89 deletions
diff --git a/build-aux/measurestack/analyze.py b/build-aux/measurestack/analyze.py index 8100813..a93874f 100644 --- a/build-aux/measurestack/analyze.py +++ b/build-aux/measurestack/analyze.py @@ -150,12 +150,38 @@ class AnalyzeResult(typing.NamedTuple): included_funcs: set[QName] +class SkipModel(typing.NamedTuple): + """Running the skipmodel calls `.fn(chain, ...)` with the chain + consisting of the last `.nchain` items (if .nchain is an int), or + the chain starting with the *last* occurance of `.nchain` (if + .nchain is a collection). If the chain is not that long or does + not contain a member of the collection, then .fn is not called and + the call is *not* skipped. + + """ + + nchain: int | typing.Collection[BaseName] + fn: typing.Callable[[typing.Sequence[QName], QName], bool] + + def __call__(self, chain: typing.Sequence[QName], call: QName) -> tuple[bool, int]: + if isinstance(self.nchain, int): + if len(chain) >= self.nchain: + _chain = chain[-self.nchain :] + return self.fn(_chain, call), len(_chain) + else: + for i in reversed(range(len(chain))): + if chain[i].base() in self.nchain: + _chain = chain[i - 1 :] + return self.fn(_chain, call), len(_chain) + return False, 0 + + class Application(typing.Protocol): def extra_nodes(self) -> typing.Collection[Node]: ... def indirect_callees( self, elem: vcg.VCGElem ) -> tuple[typing.Collection[QName], bool]: ... - def skip_call(self, chain: typing.Sequence[QName], funcname: QName) -> bool: ... + def skipmodels(self) -> dict[BaseName, SkipModel]: ... # code ######################################################################### @@ -315,31 +341,20 @@ def analyze( track_inclusion: bool = True - def nstatic( - orig_funcname: QName, - chain: typing.Sequence[QName] = (), - missing_ok: bool = False, - ) -> int: + skipmodels = app.skipmodels() + for name, model in skipmodels.items(): + if isinstance(model.nchain, int): + assert model.nchain > 1 + else: + assert len(model.nchain) > 0 + + _nstatic_cache: dict[QName, int] = {} + + def _nstatic(chain: list[QName], funcname: QName) -> tuple[int, int]: nonlocal dbg nonlocal track_inclusion - funcname = graphdata.resolve_funcname(orig_funcname) - if not funcname: - if chain and app.skip_call(chain, orig_funcname): - if dbg: - print(f"//dbg: {'- '*len(chain)}{orig_funcname}\tskip missing") - return 0 - if not missing_ok: - missing.add(orig_funcname) - if dbg: - print(f"//dbg: {'- '*len(chain)}{orig_funcname}\tmissing") - return 0 - if chain and app.skip_call(chain, funcname): - if dbg: - print(f"//dbg: {'- '*len(chain)}{orig_funcname}\tskip") - return 0 - - if len(chain) == cfg_max_call_depth: - raise ValueError(f"max call depth exceeded: {[*chain, funcname]}") + + assert funcname in graphdata.graph node = graphdata.graph[funcname] if dbg: @@ -348,15 +363,57 @@ def analyze( dynamic.add(funcname) if track_inclusion: included_funcs.add(funcname) - return node.nstatic + max( - [ - 0, - *[ - nstatic(call, [*chain, funcname], missing_ok) - for call, missing_ok in node.calls.items() - ], - ] - ) + + max_call_nstatic = 0 + max_call_nchain = 0 + + if node.calls: + skipmodel = skipmodels.get(funcname.base()) + chain.append(funcname) + if len(chain) == cfg_max_call_depth: + raise ValueError(f"max call depth exceeded: {chain}") + for call_orig_qname, call_missing_ok in node.calls.items(): + skip_nchain = 0 + # 1. Resolve + call_qname = graphdata.resolve_funcname(call_orig_qname) + if not call_qname: + if skipmodel: + skip, _ = skipmodel(chain, call_orig_qname) + if skip: + if dbg: + print( + f"//dbg: {'- '*len(chain)}{call_orig_qname}\tskip missing" + ) + continue + if not call_missing_ok: + missing.add(call_orig_qname) + if dbg: + print(f"//dbg: {'- '*len(chain)}{call_orig_qname}\tmissing") + continue + + # 2. Skip + if skipmodel: + skip, skip_nchain = skipmodel(chain, call_qname) + max_call_nchain = max(max_call_nchain, skip_nchain) + if skip: + if dbg: + print(f"//dbg: {'- '*len(chain)}{call_qname}\tskip") + continue + + # 3. Call + if skip_nchain == 0 and call_qname in _nstatic_cache: + max_call_nstatic = max(max_call_nstatic, _nstatic_cache[call_qname]) + else: + call_nstatic, call_nchain = _nstatic(chain, call_qname) + max_call_nstatic = max(max_call_nstatic, call_nstatic) + max_call_nchain = max(max_call_nchain, call_nchain) + if skip_nchain == 0 and call_nchain == 0: + _nstatic_cache[call_qname] = call_nstatic + chain.pop() + return node.nstatic + max_call_nstatic, max(0, max_call_nchain - 1) + + def nstatic(funcname: QName) -> int: + return _nstatic([], funcname)[0] groups: dict[str, AnalyzeResultGroup] = {} for grp_name, grp_filter in app_func_filters.items(): diff --git a/build-aux/measurestack/app_plugins.py b/build-aux/measurestack/app_plugins.py index 7eb7706..519e6b8 100644 --- a/build-aux/measurestack/app_plugins.py +++ b/build-aux/measurestack/app_plugins.py @@ -6,7 +6,7 @@ import re import typing -from . import util +from . import analyze, util from .analyze import BaseName, Node, QName from .util import synthetic_node @@ -51,8 +51,8 @@ class CmdPlugin: return [QName("get_root")], False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} re_comment = re.compile(r"/\*.*?\*/") @@ -130,8 +130,8 @@ class LibObjPlugin: ], False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} class LibHWPlugin: @@ -199,8 +199,8 @@ class LibHWPlugin: ], False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} class LibCRPlugin: @@ -223,8 +223,8 @@ class LibCRPlugin: ) -> tuple[typing.Collection[QName], bool] | None: return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} class LibCRIPCPlugin: @@ -252,8 +252,8 @@ class LibCRIPCPlugin: ], False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} re_tmessage_handler = re.compile( @@ -263,6 +263,9 @@ re_lib9p_msg_entry = re.compile(r"^\s*_MSG_(?:[A-Z]+)\((?P<typ>\S+)\),$") re_lib9p_caller = re.compile( r"^lib9p_(?P<grp>[TR])msg_(?P<meth>validate|unmarshal|marshal)$" ) +re_lib9p_callee = re.compile( + r"^(?P<meth>validate|unmarshal|marshal)_(?P<msg>(?P<grp>[TR]).*)$" +) class Lib9PPlugin: @@ -372,21 +375,48 @@ class Lib9PPlugin: return [QName(f"{meth}_{msg}") for msg in self.lib9p_msgs], True return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - if "lib9p/srv.c:srv_util_pathfree" in str(call): - assert isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int) - if len(chain) >= self.CONFIG_9P_SRV_MAX_DEPTH and all( - ("lib9p/srv.c:srv_util_pathfree" in str(c)) + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + ret: dict[BaseName, analyze.SkipModel] = { + BaseName("_lib9p_validate"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + BaseName("_lib9p_unmarshal"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + BaseName("_lib9p_marshal"): analyze.SkipModel( + 2, + self._skipmodel__lib9p_validate_unmarshal_marshal, + ), + } + if isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int): + ret[BaseName("srv_util_pathfree")] = analyze.SkipModel( + self.CONFIG_9P_SRV_MAX_DEPTH, + self._skipmodel_srv_util_pathfree, + ) + return ret + + def _skipmodel__lib9p_validate_unmarshal_marshal( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + m_caller = re_lib9p_caller.fullmatch(str(chain[-2].base())) + assert m_caller + + m_callee = re_lib9p_callee.fullmatch(str(call.base())) + if not m_callee: + return False + return m_caller.group("grp") != m_callee.group("grp") + + def _skipmodel_srv_util_pathfree( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + assert isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int) + if call.base() == BaseName("srv_util_pathfree"): + return len(chain) >= self.CONFIG_9P_SRV_MAX_DEPTH and all( + c.base() == BaseName("srv_util_pathfree") for c in chain[-self.CONFIG_9P_SRV_MAX_DEPTH :] - ): - return True - wrapper = next((c for c in chain if re_lib9p_caller.match(str(c))), None) - if wrapper: - m = re_lib9p_caller.match(str(wrapper)) - assert m - deny = m.group("meth") + "_" + ("R" if m.group("grp") == "T" else "T") - if str(call.base()).startswith(deny): - return True + ) return False @@ -408,14 +438,20 @@ class LibMiscPlugin: ) -> tuple[typing.Collection[QName], bool] | None: return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - if ( - len(chain) > 1 - and chain[-1].base() == BaseName("__assert_msg_fail") - and call.base() == BaseName("__lm_printf") - and any(c.base() == BaseName("__assert_msg_fail") for c in chain[:-1]) - ): - return True + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return { + BaseName("__assert_msg_fail"): analyze.SkipModel( + {BaseName("__assert_msg_fail")}, self._skipmodel___assert_msg_fail + ), + } + + def _skipmodel___assert_msg_fail( + self, chain: typing.Sequence[QName], call: QName + ) -> bool: + if call.base() in [BaseName("__lm_printf")]: + return any( + c.base() == BaseName("__assert_msg_fail") for c in reversed(chain[:-1]) + ) return False @@ -462,23 +498,45 @@ class PicoFmtPlugin: return [x.as_qname() for x in self.known_fct.values()], False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + ret: dict[BaseName, analyze.SkipModel] = { + BaseName("_out_rev"): analyze.SkipModel( + self.known_out.keys(), self._skipmodel_outcaller + ), + BaseName("_etoa"): analyze.SkipModel( + self.known_out.keys(), self._skipmodel_outcaller + ), + BaseName("_vsnprintf"): analyze.SkipModel( + self.known_out.keys(), self._skipmodel_outcaller + ), + BaseName("_out_fct"): analyze.SkipModel( + self.known_fct.keys(), self._skipmodel__out_fct + ), + } + return ret + + def _skipmodel_outcaller(self, chain: typing.Sequence[QName], call: QName) -> bool: if call.base() in self.known_out.values(): out: BaseName | None = None - for pcall in chain: + for pcall in reversed(chain): if pcall.base() in self.known_out: out = self.known_out[pcall.base()] - if out == BaseName("_out_buffer") and call.base() == BaseName( - "_out_null" - ): # XXX: Gross hack - out = BaseName("_out_null") - return call.base() != out + if out == BaseName("_out_buffer") and call.base() == BaseName( + "_out_null" + ): # XXX: Gross hack + out = BaseName("_out_null") + return call.base() != out + return True + return False + + def _skipmodel__out_fct(self, chain: typing.Sequence[QName], call: QName) -> bool: if call.base() in self.known_fct.values(): fct: BaseName | None = None - for pcall in chain: + for pcall in reversed(chain): if pcall.base() in self.known_fct: fct = self.known_fct[pcall.base()] - return call.base() != fct + return call.base() != fct + return True return False @@ -588,8 +646,8 @@ class PicoSDKPlugin: return self.app_preinit_array, False return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} def extra_nodes(self) -> typing.Collection[Node]: ret = [] @@ -820,8 +878,8 @@ class TinyUSBDevicePlugin: return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} class NewlibPlugin: @@ -887,8 +945,8 @@ class NewlibPlugin: ) -> tuple[typing.Collection[QName], bool] | None: return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} class LibGCCPlugin: @@ -917,5 +975,5 @@ class LibGCCPlugin: ) -> tuple[typing.Collection[QName], bool] | None: return None - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: - return False + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + return {} diff --git a/build-aux/measurestack/util.py b/build-aux/measurestack/util.py index 5367da9..47b2617 100644 --- a/build-aux/measurestack/util.py +++ b/build-aux/measurestack/util.py @@ -6,7 +6,7 @@ import re import typing -from . import vcg +from . import analyze, vcg from .analyze import BaseName, Node, QName # pylint: disable=unused-variable @@ -82,7 +82,7 @@ class Plugin(typing.Protocol): def indirect_callees( self, loc: str, line: str ) -> tuple[typing.Collection[QName], bool] | None: ... - def skip_call(self, chain: typing.Sequence[QName], call: QName) -> bool: ... + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: ... class PluginApplication: @@ -118,8 +118,8 @@ class PluginApplication: placeholder += " at " + self._location_xform(elem.attrs.get("label", "")) return [QName(placeholder)], False - def skip_call(self, chain: typing.Sequence[QName], funcname: QName) -> bool: + def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: + ret: dict[BaseName, analyze.SkipModel] = {} for plugin in self._plugins: - if plugin.skip_call(chain, funcname): - return True - return False + ret.update(plugin.skipmodels()) + return ret |