Bug 351410 - [rename] Renaming a "getName" method takes ages to complete "checking preconditions" phase...
Summary: [rename] Renaming a "getName" method takes ages to complete "checking precond...
Status: ASSIGNED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 3.7   Edit
Hardware: All All
: P3 major with 9 votes (vote)
Target Milestone: ---   Edit
Assignee: JDT-UI-Inbox CLA
QA Contact:
URL:
Whiteboard: stalebug
Keywords: performance
: 517109 (view as bug list)
Depends on: 420055 481796
Blocks:
  Show dependency tree
 
Reported: 2011-07-07 05:25 EDT by Mauro Molinari CLA
Modified: 2024-01-24 22:15 EST (History)
17 users (show)

See Also:


Attachments
proposed patch (5.25 KB, patch)
2013-10-15 10:41 EDT, Nikolay Metchev CLA
no flags Details | Diff
proposed patch (7.96 KB, patch)
2013-10-15 11:45 EDT, Nikolay Metchev CLA
no flags Details | Diff
proposed patch (22.57 KB, patch)
2013-10-23 10:17 EDT, Nikolay Metchev CLA
no flags Details | Diff
Cursory performance measurements for change 116888, showing about 4x improvement for simple cases. (6.59 MB, application/zip)
2018-03-30 03:08 EDT, Simeon Andreev CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Mauro Molinari CLA 2011-07-07 05:25:58 EDT
STEPS TO REPRO
1) create a Java project
2) create the following class in it:

package a;

public class MyClass {
  public String getName()
  {
	  return null;
  }
}

3) try to refactor getName() method name to getSomething()

OBSERVED BEHAVIOUR
The "checking preconditions" phase takes tens of seconds to complete in the test project in an almost empty workspace. Using a real-world project in a workspace with about 50 open projects, it takes AGES (that is: tens of minutes).
The problems seems to rely on the name of the method, which is "getName()". If the name is something else, the "checking precondition" phase is slow, but succeeds within some tens of seconds even in the huge workspace case.

EXPECTED BEHAVIOUR
I don't know what the "checking preconditions" phase does, but if getName() is public and MyClass is in a project which is not referenced by any other project, I think the refactoring process should be almost instantaneous, especially because the test project is made up of just MyClass class... In a real-world case I would expect it to terminate quickly anyway...
Comment 1 Mauro Molinari CLA 2011-07-07 05:32:04 EDT
The dump test has been performed using Eclipse 3.7 on a almost new workspace (no Java Projects in it).

The real-world case was reproduced with Eclipse 3.6.2. While waiting for "checking preconditions" I tried to capture some thread dumps. Here is one:

Stack trace: 
org.eclipse.jdt.internal.core.index.DiskIndex.addQueryResults(DiskIndex.java:203)
org.eclipse.jdt.internal.core.index.Index.query(Index.java:138)
org.eclipse.jdt.internal.core.search.matching.SuperTypeReferencePattern.queryIn(SuperTypeReferencePattern.java:272)
org.eclipse.jdt.core.search.SearchPattern.findIndexMatches(SearchPattern.java:2295)
org.eclipse.jdt.internal.core.search.matching.MatchLocator.findIndexMatches(MatchLocator.java:338)
org.eclipse.jdt.internal.core.search.PatternSearchJob.search(PatternSearchJob.java:96)
org.eclipse.jdt.internal.core.search.SubTypeSearchJob.search(SubTypeSearchJob.java:44)
org.eclipse.jdt.internal.core.search.PatternSearchJob.execute(PatternSearchJob.java:63)
org.eclipse.jdt.internal.core.search.processing.JobManager.performConcurrentJob(JobManager.java:276)
org.eclipse.jdt.internal.core.hierarchy.IndexBasedHierarchyBuilder.searchAllPossibleSubTypes(IndexBasedHierarchyBuilder.java:523)
org.eclipse.jdt.internal.core.hierarchy.IndexBasedHierarchyBuilder.determinePossibleSubTypes(IndexBasedHierarchyBuilder.java:406)
org.eclipse.jdt.internal.core.hierarchy.IndexBasedHierarchyBuilder.build(IndexBasedHierarchyBuilder.java:120)
org.eclipse.jdt.internal.core.hierarchy.TypeHierarchy.compute(TypeHierarchy.java:300)
org.eclipse.jdt.internal.core.hierarchy.TypeHierarchy.refresh(TypeHierarchy.java:1263)
   - locked org.eclipse.jdt.internal.core.hierarchy.TypeHierarchy@2b77f99d
org.eclipse.jdt.internal.core.CreateTypeHierarchyOperation.executeOperation(CreateTypeHierarchyOperation.java:90)
org.eclipse.jdt.internal.core.JavaModelOperation.run(JavaModelOperation.java:728)
org.eclipse.jdt.internal.core.JavaModelOperation.runOperation(JavaModelOperation.java:788)
org.eclipse.jdt.internal.core.BinaryType.newTypeHierarchy(BinaryType.java:918)
org.eclipse.jdt.internal.corext.refactoring.rename.RippleMethodFinder2.getCachedHierarchy(RippleMethodFinder2.java:321)
org.eclipse.jdt.internal.corext.refactoring.rename.RippleMethodFinder2.findAllRippleMethods(RippleMethodFinder2.java:274)
org.eclipse.jdt.internal.corext.refactoring.rename.RippleMethodFinder2.getAllRippleMethods(RippleMethodFinder2.java:168)
org.eclipse.jdt.internal.corext.refactoring.rename.RippleMethodFinder2.getRelatedMethods(RippleMethodFinder2.java:161)
org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.initializeMethodsToRename(RenameMethodProcessor.java:236)
org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.doCheckFinalConditions(RenameMethodProcessor.java:360)
org.eclipse.jdt.internal.corext.refactoring.rename.RenameVirtualMethodProcessor.doCheckFinalConditions(RenameVirtualMethodProcessor.java:143)
org.eclipse.jdt.internal.corext.refactoring.rename.JavaRenameProcessor.checkFinalConditions(JavaRenameProcessor.java:46)
org.eclipse.ltk.core.refactoring.participants.ProcessorBasedRefactoring.checkFinalConditions(ProcessorBasedRefactoring.java:224)
org.eclipse.ltk.core.refactoring.Refactoring.checkAllConditions(Refactoring.java:160)
org.eclipse.jdt.internal.ui.refactoring.RefactoringExecutionHelper$Operation.run(RefactoringExecutionHelper.java:80)
org.eclipse.jdt.internal.core.BatchOperation.executeOperation(BatchOperation.java:39)
org.eclipse.jdt.internal.core.JavaModelOperation.run(JavaModelOperation.java:728)
org.eclipse.core.internal.resources.Workspace.run(Workspace.java:1975)
org.eclipse.jdt.core.JavaCore.run(JavaCore.java:4777)
org.eclipse.jdt.internal.ui.actions.WorkbenchRunnableAdapter.run(WorkbenchRunnableAdapter.java:106)
org.eclipse.jface.operation.ModalContext$ModalContextThread.run(ModalContext.java:121)

Please note that I have an Athlon 64 X2 5200+ processor with 4GB RAM and a Corsair Force F60 SSD in it, so I think the system performance is adeguate.
Using 64-bit builds of Eclipse and a Java 1.6.0_22 64-bit JRE.
Comment 2 Mauro Molinari CLA 2011-07-07 05:32:35 EDT
(In reply to comment #1)
> The dump test has been performed using Eclipse 3.7 on a almost new workspace
> (no Java Projects in it).

Sorry, I meant "the dumb test has been performed...".
Comment 3 Ayushman Jain CLA 2011-07-07 06:40:00 EDT
Wow, looks real slow indeed! :(
Satyam, can you please check? Thanks!
Comment 4 Satyam Kandula CLA 2011-08-16 04:36:15 EDT
getName() is present in many classes in the JVM's rt.jar. Refactor calls search for method declarations of getName() and not MyClass#getName(), causing the slow down. Markus, is there a reason for this?
Comment 5 Markus Keller CLA 2011-10-04 14:19:15 EDT
(In reply to comment #4)
Yes. The Rename refactoring has to walk up and down the hierarchy to find all methods that are connected to the renamed method. That's because the method could be inherited from multiple supertypes, and we have to find the whole "ripple" of methods that have to be renamed together.

Since walking down the type hierarchy is extremely expensive (need to create subtype hierarchies), the current implementation first finds all methods with matching signatures and then connects them via supertype hierarchies.
Comment 6 Markus Keller CLA 2011-10-04 14:58:07 EDT
Looking at the code again, I think the expensive part is this situation:

package a;

public class MyClass implements I {
	public String getXXX() {
		return null;
	}
}

interface I {
	public String getXXX();
}

interface J {
	public String getXXX();
}

interface Both extends I, J {

}


The problem is that J#getXXX() also needs to be renamed, but we cannot find out whether there's a "connector" like interface "Both" without creating subtype hierarchies.

Taking this bug to JDT/UI, since the problem is in our implementation. We have to check if we can avoid processing the rt.jar in this case. Maybe fixing the
"//TODO: would only need subtype hierarchies of all top-of-ripple relatedTypesToProcess" in RippleMethodFinder2 would also help.
Comment 7 Dani Megert CLA 2013-10-15 06:10:50 EDT
Hi Nikolay, here's another bug for you.
Comment 8 Nikolay Metchev CLA 2013-10-15 08:11:44 EDT
I am going to give it a go. Wish me luck.
Comment 9 Mauro Molinari CLA 2013-10-15 08:26:44 EDT
Good luck! :-D
And, seriously, thanks in advance!
Comment 10 Nikolay Metchev CLA 2013-10-15 09:41:04 EDT
Hello,
Can I ask if RippleMethodFinderTests is supposed to be working?
It seems that for some reason on my machine when I run all the unit tests some of them fail because they interfere with each other. However when I run each test individually they pass. It is a little tricky to know if I have broken stuff.
Comment 11 Nikolay Metchev CLA 2013-10-15 10:41:36 EDT
Created attachment 236488 [details]
proposed patch

This contribution complies with http://www.eclipse.org/legal/CoO.php

It seems to me there is no need to search rt.jar and other system jars for ripple methods. So I have changed the code not to do that. I ran all the unit tests that use RippleMethodFinder2 and all of them passed. 
The 2nd change which is very similar is to create a RefactoringSearchScope that only search in sources. Seeing as we are refactoring it seemed to me we don't care about things we cannot refactor (binary sources). 

Is my reasoning sound?
Comment 12 Nikolay Metchev CLA 2013-10-15 11:45:37 EDT
Created attachment 236491 [details]
proposed patch

This contribution complies with http://www.eclipse.org/legal/CoO.php

Forgot to update the headers of the changed files.
Comment 13 Nikolay Metchev CLA 2013-10-15 12:13:08 EDT
Looks like the 2nd change breaks BinaryReferenceTests. The only reason refactorings search binary sources is to report potential errors. I still think the 1st change is valid though. It does speed things up and it doesn't break any tests.
Comment 14 Nikolay Metchev CLA 2013-10-17 15:41:43 EDT
Noopur, please ignore my patch. I have a much better idea.
Comment 15 Noopur Gupta CLA 2013-10-21 05:39:40 EDT
Comment on attachment 236491 [details]
proposed patch

(In reply to Nikolay Metchev from comment #14)
> Noopur, please ignore my patch. I have a much better idea.
Comment 16 Nikolay Metchev CLA 2013-10-22 05:57:42 EDT
My idea was to use structural search instead of the SearchEngine. So I tried replacing RippleMethodFinder2.findAllDeclarations with a traversal up and down the hierarchy to find all overrides. This sped things along but several unit tests failed including RenameVirtualMethodInClassTests.testGenerics1. This prompted me to file bug 420055. I think for now I have to wait for bug 420055 to be fixed before I can proceed here. I am going to try to fix it myself but it doesn't look like it is a quick fix.
Comment 17 Nikolay Metchev CLA 2013-10-23 10:17:59 EDT
Created attachment 236802 [details]
proposed patch

This contribution complies with http://www.eclipse.org/legal/CoO.php

I have spent some time profiling and here is what I have discovered.

The slow bit is definitely the building of the hierarchy. So if we can reduce the number of time we have to do that we should get a performance boost.

At the moment RippleMethodFinder2 in order to find all declarations it first uses the search engine to find all methods with the desired signature and then performs a union find in order to determine if they are "married" to the original method. In order to perform the union find it has to invoke the hierarchy building for every search result. Lets call this Method 1.

With this patch I have added the option of finding all declarations in a different way. By building the hierarchy of the original method and traversing it up and down in order to find any married methods. This then does not need to perform the subsequent union find. Lets call this Method 2.

To test if the new option is implemented correctly I ran all the unit tests with it enabled. That is how I discovered bug 420055. 

Clearly Method 1 will perform better than method 2 if there are very few declarations that match the initial search and the source method lives is in a very extensive hierarchy. 

Method 2 will perform better if the target declaration is in a very simple hierarchy.

With this patch I have decided to use a heuristic approach based on the number of search results that come back from the search engine. If the number of declarations is more than 20 It uses Method 2 otherwise the implementation remains unchanged. This will not break any unit tests because the new code will only be executed in one test case that is also added as part of this patch. 

I removed the fDeclarations field. The contents of this field get put into fTypeToMethod so that should make a slight difference in a few select corner cases.

Also I changed the RippleMethodFinderTests to extend RefactoringTest and added it to AllRefactoringTests. This solved the problem of unit tests interfering with each other (not sure why AbstractCUTestCase tests interfere with each other, perhaps there is a bug somewhere). 

Also in this patch are a couple of commented out test cases for bug 420055.

I look forward to hearing feedback.
Comment 18 Nikolay Metchev CLA 2013-10-24 05:17:58 EDT
I have thought about my code and I have realized there is a flaw in it. I will upload a new patch when I have figured out the correct solution.
Comment 19 Noopur Gupta CLA 2013-11-18 07:06:53 EST
Comment on attachment 236802 [details]
proposed patch

Nikolay, please take your time for coming up with a correct solution.
The focus is now on Java 8 work, hence further feedback/review would also take some time.
Comment 20 Nikolay Metchev CLA 2013-11-18 12:18:04 EST
Hello Noopur,

I was hoping that my patch for bug 99622 be committed first before I carry on with this bug. That patch changes RippleMethodFinder2 significantly enough to make it a necessary prerequisite.
Comment 21 Dani Megert CLA 2017-05-23 04:42:08 EDT
*** Bug 517109 has been marked as a duplicate of this bug. ***
Comment 22 Andrey Loskutov CLA 2017-05-23 04:49:36 EDT
The problem is still there in 4.6.3.

Nikolay, do you still plan to work on the patch? If not, we should re-assign this bug back to the JDT team.
Comment 23 Nikolay Metchev CLA 2017-05-23 15:28:17 EDT
After 4 years of waiting for someone at eclipse to take notice of my contributions I finally gave up and have to conclude that the project is under resourced and effectively dead. I've moved to Idea after 15 years of championing eclipse.
Comment 24 Eclipse Genie CLA 2018-02-06 13:38:38 EST
New Gerrit change created: https://git.eclipse.org/r/116813
Comment 25 Simeon Andreev CLA 2018-02-06 14:16:15 EST
OK, I browsed through the code and Nikolay's ideas. They sound good to me.

While profiling the code, I noticed some sub-optimal use of collections, for which I created a small patch. This helps a bit, but nothing too drastic.

Once we merge this, I'll go with Nikolay's idea to remove the system binaries from the method search (Comment 11). This gives a massive boost and doesn't seem to be too dangerous. I imagine 99.9% of the user base will never run into a problem with this change, to be on the safe side I guess we can add a preference.

If the above changes go well, I'll profile again. I'll determine whether we must go with Nikolay's idea to try a different strategy if the current one finds too many hits before building hierarchies (Comment 17). This is a bit heavy-weight as far as changes go, and I'll need some extra time to put the idea into code (I don't see the patches anymore).
Comment 26 Stephan Herrmann CLA 2018-02-06 15:15:42 EST
(In reply to Simeon Andreev from comment #25)
> Once we merge this, I'll go with Nikolay's idea to remove the system
> binaries from the method search (Comment 11).

With that, will you still detect when a user tries to rename a method that overrides a method from the JRE?

> This gives a massive boost and
> doesn't seem to be too dangerous. I imagine 99.9% of the user base will
> never run into a problem with this change, to be on the safe side I guess we
> can add a preference.

A user would first need to know that they are part of the "0.1%", in order to toggle that preference, but when checks are disabled they may never notice. Doesn't sound like any added safety to me ;p
Comment 27 Andrey Loskutov CLA 2018-02-06 16:46:49 EST
I must confess I haven't seen the code in question yet. Simeon, can you briefly describe the current implementation and why it searches through the binaries? I guess only to detect if the target method name has a clash with some super interface?

Now let assume we skip the comprehensive search all together, do a quick change and *after* the change check if we have a collision - wouldn't this be faster for 99.9% of users? And for the case of a clash we do a quick rollback?

Also I imagine we can pick up the low hanging fruit and don't do a search if the current type is not implementing/extending anything?
Comment 28 Simeon Andreev CLA 2018-02-07 07:19:16 EST
(In reply to Stephan Herrmann from comment #26)
> (In reply to Simeon Andreev from comment #25)
> > Once we merge this, I'll go with Nikolay's idea to remove the system
> > binaries from the method search (Comment 11).
> 
> With that, will you still detect when a user tries to rename a method that
> overrides a method from the JRE?

Trying to rename overriden system library methods still yields an error message,
due to a check in RenameMethodProcessor.doCheckFinalConditions which
delegates to Checks.isAvailable. This in turn will check if the method
is read only, yielding an error. I tried this e.g. with overriding a method from FileInputStream.


> > This gives a massive boost and
> > doesn't seem to be too dangerous. I imagine 99.9% of the user base will
> > never run into a problem with this change, to be on the safe side I guess we
> > can add a preference.
> 
> A user would first need to know that they are part of the "0.1%", in order
> to toggle that preference, but when checks are disabled they may never
> notice. Doesn't sound like any added safety to me ;p

To be honest, I have trouble imagining the error case here. As mentioned, inheriting from a JRE class and renaming a method fails. I think a developer would need to work in a project that contains JRE sources, e.g. java.util.List, and rename a method. If, supposedly, this goes through, then all the binaries which depend on the previous definition become invalid. I can only imagine this to be a problem for a JRE developer, if they use Eclipse, and they rename internal methods.
Comment 29 Simeon Andreev CLA 2018-02-07 10:01:22 EST
(In reply to Andrey Loskutov from comment #27)
> I must confess I haven't seen the code in question yet. Simeon, can you
> briefly describe the current implementation and why it searches through the
> binaries? I guess only to detect if the target method name has a clash with
> some super interface?

Please check Comment 17 from Nikolay Metchev, Nikolay explained what the code does:

> At the moment RippleMethodFinder2 in order to find all declarations it first
> uses the search engine to find all methods with the desired signature and
> then performs a union find in order to determine if they are "married" to
> the original method. In order to perform the union find it has to invoke the
> hierarchy building for every search result.

Please check also Comment 5 and Comment 6 from Markus Keller, those explain why all occurrences are listed (and necessary).

Though personally I don't agree with the statement in Comment 6, that also Interface J should be changed. Clearly the method which is being renamed is I.getXXX. If the developer got themselves in the situation where they implement two different interfaces with the same method, from my perspective they already lost.

> Now let assume we skip the comprehensive search all together, do a quick
> change and *after* the change check if we have a collision - wouldn't this
> be faster for 99.9% of users? And for the case of a clash we do a quick
> rollback?

Theoretically the code already does this. It checks that the source can still compile. I need to check whether it will check this *for all changed sources*. If so this sounds relatively safe.

An example where this wouldn't working on the Eclipse platform, and changing e.g. jdt.core, without having e.g. jdt.ui open. If renaming API in this situation, its possible to break jdt.ui sources which are not open, since those will not be renamed. Those will not be validated for compile errors. Maybe such cases are easier to check than the current code. In that case it should be possible to check explicitly for this case, and rely on the does-it-compile-after-rename-check for the rest.

> Also I imagine we can pick up the low hanging fruit and don't do a search if
> the current type is not implementing/extending anything?

Yes, that can definitely be done. It would complicate the code a bit more though, and I'm not sure how much we gain. I think we should aim to improve the code for all cases, and if we reach a brick-wall, we can resort to this option.
Comment 30 Simeon Andreev CLA 2018-02-07 15:12:41 EST
Looks like return types of methods are ignored, this may explain why so many getName hits are seen. I don't see a way around this though, e.g. due to

interface I {
    Number get();
}

interface J extends I {
    Double get();
}

IMethod doesn't allow for easy checking if its return type has a specific super type? IMethod.getReturnType only returns a string...
Comment 31 Eclipse Genie CLA 2018-02-07 17:06:23 EST
New Gerrit change created: https://git.eclipse.org/r/116888
Comment 32 Simeon Andreev CLA 2018-02-07 17:14:21 EST
I think Andrey's idea is quite simple to extend to more common cases, see https://git.eclipse.org/r/#/c/116888.

Today I "foolishly" refactor renamed a dispose() method in a class which didn't derive from *anything*. The operation takes 90s for our code base.

With the change from the commit, the operation takes 10s.

The patch is very ham handed, it will compute a hierarchy of all supposedly unrelated methods and then cut it with the hierarchy of the method under rename. With some set operations / a bit more union finding, I believe its possible to improve on this, so that a bit more complex cases also benefit.
Comment 33 Simeon Andreev CLA 2018-02-26 04:22:11 EST
Unfortunately I have little to no time to invest here, I'll try to find some in the last two weeks of March.

Hopefully enough to propagate the current change, which already speeds up simple cases a lot.
Comment 34 Simeon Andreev CLA 2018-03-30 03:08:44 EDT
Created attachment 273370 [details]
Cursory performance measurements for change 116888, showing about 4x improvement for simple cases.
Comment 35 Simeon Andreev CLA 2018-04-13 04:23:57 EDT
Dear JDT maintainers,

I've prepared a patch which takes simple renaming cases into account, reducing rename time to 1/4 of its original time (for said simple cases).

This is likely patch 1 out of 2 patches to fix this defect. The next step would be to fully implement "Method 2" suggested by Nikolay Metchev in Comment 17 (so far the improvement heavily depends on this method, but does not fully implement it).

If you find time to review and hopefully merge, please do so.

Best regards and thanks,
Simeon
Comment 37 Eclipse Genie CLA 2020-09-07 17:58:51 EDT
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 38 Eclipse Genie CLA 2023-01-20 11:09:34 EST
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 39 Radek Czyz CLA 2024-01-24 22:15:51 EST
(In reply to Eclipse Genie from comment #38)
> This bug hasn't had any activity in quite some time. Maybe the problem got
> resolved

As someone who just tried to rename method "get", the bug is definitely here in 2023-12.

Moreover the process cannot be cancelled so killing eclipse is the only way to recover.

It's pretty obvious that only the types connected by hierarchy to the current class needs to be considered, but I appreciate it might not be trivial to use this information...