Jamie and Cédric,
Following up on this earlier conversation about how a shared CompilerConfiguration object impacts parsing performance with ImportCustomizers, I believe I figured out what is going on:
The original executive summary was that applying many ImportCustomizers to a script was very slow (costing us hundreds of ms in parse time per script). If I share a single CompilerConfiguration object across multiple scripts, the cost of using the ImportCustomizers suddenly goes down to almost zero on the second and subsequent parses. This is with Groovy 2.3.6, for what it is worth.
I have since found that sharing the entire CompilerConfiguration is not even necessary, since it is only the ImportCustomizer itself that "helped" with performance. In other words, if you create multiple CompilerConfiguration objects but attach the same ImportCustomizer to both, then you'll see the higher performance demonstrated in my test case below. But if you create an entirely new ImportCustomizer every time that has an identical set of imports as the original ImportCustomizer object, then your performance will take a big hit.
As far as I can tell, this speed differential occurs because the ImportCustomizer is mutable, and when I run the Groovy parser with an ImportCustomizer attached, the ImportCustomizer object is actually changed (or, more precisely, the objects it references are changed). I think this happens in ResolveVisitor as part of the class resolution process.
That also presumably explains why the second and subsequent parses are so fast (classes are already resolved)...although I admit that the 300 ms hit for parsing 35 classes in the very first invocation still seems slow.
I came to the above conclusion about mutability of the ImportCustomizer after stumbling across a race condition: if I create a single ImportCustomizer and attach it to scripts that are being parsed simultaneously on different threads, I start getting random errors with my script class's constructor being unable to resolve many of the class names listed in the ImportCustomizer imports. The errors are somewhat mangled by having "." randomly changed to "$"' in parts of the package name (even though no inner classes are involved). This looks to be some side-effect of the ResolveVisitor#resolve() process, since I see it replacing some strings and calling setName() on the node.
Whatever manipulation is going on there was apparently not intended to performed on shared objects, but I was able to make it work by serializing all calls to the Groovy parser until at least one good parse has been completed. After the first good parse, the types in the ImportCustomizer seem to be fully resolved and I can again parse scripts on multiple thread simultaneously with the shared ImportCustomizer.
With all of that having been written: is sharing the same ImportCustomizer across different classloaders and parse sessions still a good idea? I like the performance improvement that sharing the ImportCustomizer provides, but the mutability aspect seems like more of an inadvertent feature and/or issue that might not have been intended by the Groovy designers.
Thanks for any insight any of you can provide!