-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Performance regression: pointless-exception-statement
#8073
Comments
Cross posting possible solutions from #7939 (comment) Inference can be expensive, especially if the lru cache has reached is maxsize and old entries are overwritten. We should not call for
The goal IMO should be to capture 95% of the likely errors without the huge performance impact. |
I'm not yet able to replicate the reported performance degradations locally, when running the comparative versions of the The benchmark process I'm using is: pylint.git $ python3 -m venv .venv
pylint.git $ source .venv/bin/activate
pylint.git $ pip install -r requirements_test_min.txt
pylint.git $ export PYTHONPATH=.
pylint.git $ git checkout 89a20e2ccdb2927ac370f7c6663fc83318988e10
pylint.git $ rm -rf ~/.cache/pylint/
pylint.git $ time find .venv/lib/python3.*/site-packages/astroid/ -name '*.py' -exec python3 -m pylint -j2 --output /dev/null {} +
pylint.git $ git checkout 56121344f6c9c223d8e4477c0cdb58c46c840b4b
pylint.git $ rm -rf ~/.cache/pylint/
pylint.git $ time find .venv/lib/python3.*/site-packages/astroid/ -name '*.py' -exec python3 -m pylint -j2 --output /dev/null {} + |
The codebases are likely too small to see any impact. We do cache the results of If more calls are inferred and the cache is overwritten, inference needs to happen all over again. Combined with the non-deterministic behavior of You could try to reduce the cache size here and see if the impact becomes noticeable |
Maybe off-topic, but, while I'm experimenting with that: does increasing the cache size improve performance when |
Ok, I do see some performance degradation for #7939 when compared against the previous commit on a larger codebase; I'll make some adjustments to the original pull request to see whether they improve the situation.
|
It doesn't appear to, from an initial inspection: with an
|
A naive implementation of suffix-based name filtering before inference is applied doesn't appear to help much, if at all: - inferred = utils.safe_infer(expr)
+ inferred = None
+ if any(
+ expr.func.name.endswith(suffix)
+ for suffix in ("Error", "Exception", "Warning")
+ ):
+ inferred = utils.safe_infer(expr)
|
@cdce8p I'm unsure how to proceed with this - I was attempting to replicate and verify that performance is better without #7939 and that performance is degraded after it was merged, but I haven't managed to achieve that. I believe that there may be a performance regression but I'm not certain about the conditions required for it to appear. What can we do to find it? |
I'm planning to run some more testing with the LRU cache size reduced, as suggested, to see if that helps track down the cause. |
Saw the same. Setting --
Took me a moment to verify but a naive implementation does fix the CI issue. Time is back down to 8:00. The change I tested: 46c8a97 It probably needs to be something like this if isinstance(expr, nodes.Call):
inferred = None
name = ""
if isinstance(expr.func, nodes.Name):
name = expr.func.name
elif isinstance(expr.func, nodes.Attribute):
name = expr.func.attrname
if any(
name.endswith(suffix)
for suffix in ("Error", "Exception", "Warning")
):
inferred = utils.safe_infer(expr) |
Interesting, and good to hear, respectively - thanks for checking both of those! I'm going to spend some time experimenting with your fix - feel free to open that as a pull request, if you like? Also, but low priority: if you have time for any further testing: could you check whether #8075 also solves the problem? (it's a different approach -- and I don't think it's great -- but it could be useful to know whether it's valid) |
To maybe explain why I'm being so slow and dragging my feet on this change: I'm feeling very reluctant to combine any string-pattern-matching with inference, because it seems to me like much of the value of type inference is that we don't need to rely on any special patterns or strings. I'm trying to figure out how to reconcile that (if it's possible) and/or whether it's OK to simply accept a more straightforward solution (while anticipating that the decision might not be revisited for a long time, and that future potential confusion could result). |
Thinking aloud: could we omit the use of inference entirely for (yep, I'm still being slow. this was an early idea I had, but I dismissed it because I liked the precision of inference. now I'm navigating back towards it, because I think inference-matching-on-patterned-strings might be wasteful and not provide much value over simpler, faster matching-on-patterned-strings) |
Yes, we can. Pull request on the way soon. |
...but that may introduces some false positives, like these - so I'm navigating back towards a hybrid approach that uses string-pattern-matching first. If an expression looks like it may be an exception, then, The current approach does not rely on any explicit I'm still thinking about support for configurable match patterns and would welcome more opinions on that. |
Thank you for taking the time to analyse this issue in depth, much appreciated ! Could you give an example of what you mean by a configurable match pattern @jayaddison ? |
Thanks @Pierre-Sassoulas - I think I created quite a lot of noise, but hopefully some value, too :) Configurable match patterns relate to this suggestion by @cdce8p:
I'm not certain whether to implement this or not - I don't think it would be difficult to implement, so it's more a question of whether it's a good idea or not. |
I think if we add a config option it would be to use inference to its full capability. Frankly speed is not pylint's selling point so I think our focus is to avoid false positive, then to avoid false negative then to make the performance reasonable. Unless the performance are really really bad and make pylint unusable. Here clever filtering without using inference , then using inference should be enough, what do you think @cdce8p ? |
Every new check we add has some kind of performance impact. The check for me is if it's unreasonable for the benefit it provides. That's largely subjective though. Aside from that, I agree that it's more important to avoid false positives then to catch all issues (false negatives). To sum up, it's basically what you have implemented at the moment.
About the config option, I don't know how common it is to name custom exceptions something different but you never know what people come up with. That's why I suggested it in the first place, especially since it fairly easy to implement. It can help find more issues that would get unnoticed otherwise. |
I think this check is very valuable as it's catching clearly wrong code. I've had a warning at work, on a
Yeah clearly we're going to have false negatives on custom Exception not following the standard naming scheme if we do not implement an option and use a regex. But asking users to do configuration first is already a big pain point of pylint (while inference is the only thing that makes it stands out). If we're not using inference to its full potential because it's too costly performance wise what comparative advantage do we have compared to ruff or flake8 ? The performance are already not great anyway so it's not like the expectation of users is that pylint is going to be instantaneous. |
I do agree it's valuable, however not worth the 10-40% (even only for large code bases) if 95% of all errors could be detected with only 1-2% additional time.
Maybe I should have been more precise. The default config should be good enough and e.g. detect all issues with stdlib exceptions. The option would only be there for users who want to add / check more names if the default isn't good enough / doesn't work for them.
I don't believe flake8 would ever add a check like this. Without inference they couldn't be sure that a name actually refers to an exception. We can do that! Even if we don't catch every case.
Please don't underestimate the impact of good performance. True pylint is already slow, but that isn't a good thing. IMO it's already too slow for pre-commit as an example. I usually stop the commit after the linters and add |
While I do think that #7939 introduced a performance penalty, I haven't been able to replicate anywhere near the 40% degradation for the Home Assistant |
I agree performance is important but pylint's differentiating factor is exhaustiveness not speed, if we sacrifice exhaustiveness for performance, pylint will be mediocre in every possible aspects. You convinced me that in this case handling all the builtin exception and all the exception following the standard pattern for exception by default and having an option to handle the exception not following the standard pattern is reasonable. |
I'm optimistic that we can agree on a fix for this, whether it's #8073 or #8084, or another approach. That said: I do feel fairly strongly that it's better to provide thorough coverage for smaller projects, and allow performance optimizations for larger/more sophisticated cases, instead of reducing coverage for smaller codebases. If that opinion becomes too contentious here, then I'm fine with #7939 (the original implementation) being reverted to unblock a 2.16.0 release. |
I think Pylint could solve the halting problem and people would still complain that it was too slow. So I am in favor of the approach that flags most of the common cases as quickly as possible. |
I'll write here to try and keep the discussion together. -- Tbh. I had assumed it's good enough. While thinking about a good answer, I just looked at the common exceptions from requests.exceptions and things fell apart from there. If only 2/3 of all exception names actually contain one of Relying on inference seems to be the best we've got. It's just unfortunate that every function call needs to be inferred for that. Your solution to provide a flag is probably the better approach then. I'm just wondering if there is an alternative heuristic we can use 🤔 Assuming users (for large projects) at least use PascalCase, we might be able to use the regex for that as a fallback. It would at least filter out the functions.
|
Thanks, @cdce8p - good idea about PascalCase, that seems like an agreeable compromise -- catching all of the builtins, plus a larger set of real-world examples, while reducing the scope of configuration required. I'll take a look at your updated comments (and some of the yet-to-be-resolved threads) in #8078 and will implement that suggestion there soon. |
Sorry, I'm having some doubts again while looking into implementing those changes. I think it would be better to revert #7939 until we are confident of a more precise cause for the regression (I have an idea that it could relate to non-assignment expressions that involve attribute access ( |
I've been testing Home Assistant with this change: 73372d4 and (with A bit hard to tell for sure though with Github Actions. It's a bit flaky at the moment. Github had a performance issue a few hours ago might be just that. 3 out of 4 runs confirm it though. |
Thanks for the new check! I've just tested it with Home-Assistant and was able to find a handful of issues. Unfortunately, the current implementation has significant performance problems.
Comparing 5612134 with 89a20e2, a full pylint ci run can take between
6%
and43%
more time. The spread is likely a result of different caches as-j2
is used for the run.Originally posted by @cdce8p in #7939 (review)
The text was updated successfully, but these errors were encountered: