RFR: 8152641: Plugin to generate BMH$Species classes ahead-of-time

Vladimir Ivanov vladimir.x.ivanov at oracle.com
Wed Mar 30 17:11:19 UTC 2016


Nice work, Claes!

j.l.i part looks fine.

Best regards,
Vladimir Ivanov

On 3/30/16 7:17 PM, Claes Redestad wrote:
> Hi Peter,
>
> something like this, then:
>
> http://cr.openjdk.java.net/~redestad/8152641/webrev.05/
>
> - Added a method only used by the plugin which validates input; added a
> comment about the dependency
> - Invalid types are logged but otherwise ignored now (I bet someone
> might suggest a better way to handle user errors)
> - Some cleanup, introduced constant for class name prefix and removed
> duplicate type string shortening etc
>
> /Claes
>
> On 2016-03-30 17:17, Peter Levart wrote:
>> Hi Claes,
>>
>> webrev.04 looks good now.
>>
>> Maybe just one nit. For production quality, plugin input could be
>> verified that after expansion it is composed of just the following
>> characters: "LIJFD". Otherwise ClassWriter might generate an unusable
>> class without complaining (for example if someone sneaks-in characters
>> 'S' 'Z' 'B' or 'C')...
>>
>> Or, better yet, create another method in BMH that will be the "public"
>> API between the plugin and BMH which does the validation and calls
>> generateConcreteBMHClassBytes(). Internally in BMH the validation is
>> not necessary as the types strings are composed programmatically and
>> are guaranteed to be correct.
>>
>> Regards, Peter
>>
>> On 03/30/2016 04:15 PM, Claes Redestad wrote:
>>>
>>>
>>> On 2016-03-30 14:21, Peter Levart wrote:
>>>> Hi Claes,
>>>>
>>>> On 03/30/2016 12:53 PM, Claes Redestad wrote:
>>>>> Hi,
>>>>>
>>>>> I think Class.forName(Module, String) seemed like a nice
>>>>> efficiency/simplicity compromise, but there is some usage of
>>>>> lambdas/method-refs in the Module lookup code, especially for
>>>>> exploded modules (which get exercised during JDK build). Depending
>>>>> on a lambda from code in java.lang.invoke means we fail to bootstrap.
>>>>>
>>>>> But hey, we're living in an encapsulated world now, and this is in
>>>>> java.base, so why not go directly to the BootLoader:
>>>>>
>>>>> http://cr.openjdk.java.net/~redestad/8152641/webrev.03/
>>>>>
>>>>> Since this is what's called from Class.forName(Module, String), the
>>>>> overhead should be even smaller than in your classForNameInModule
>>>>> test.
>>>>
>>>> Good idea.
>>>>
>>>>>
>>>>> If I call this final, will you say "Reviewed"? :-)
>>>>
>>>> Sorry, I don't posses the powers. :-)
>>>>
>>>> I could say "rEVIEWED", but...
>>>>
>>>> In the plugin, the input is shortened speciesTypes strings. What
>>>> guarantees that they really are normalized? For example, If one
>>>> specifies "LLL" as input, it will get expanded into "LLL", the
>>>> generated class will have "_L3" as a name suffix, but you will pack
>>>> it in the image with "_LLL.class" filename suffix.
>>>>
>>>> That's another reason why a method in BoundMethodHandle$Factory with
>>>> the following signature:
>>>>
>>>> Map.Entry<String, byte[]> generateConcreteBMHClassBytes(String types);
>>>>
>>>> ...would be a good idea. It would return class bytes and the name of
>>>> the class which you could use to derive the class filename without
>>>> hardcoding the same logic in plugin and in BMH.
>>>>
>>>> You just move the "LambdaForm.shortenSignature(types)" from
>>>> getConcreteBMHClass and className/sourceFile calculation from
>>>> generateConcreteBMHClass down to generateConcreteBMHClassBytes
>>>> method and change the signatures...
>>>
>>> Yes, it makes sense to keep control over the class name inside the
>>> factory class, and this does allow specifying shortened or expanded
>>> forms (L3 vs LLL) interchangeably as input to the plugin, which
>>> reduces possibility for user errors.
>>>
>>> How about this: http://cr.openjdk.java.net/~redestad/8152641/webrev.04/
>>>
>>> /Claes
>>>
>>>>
>>>> Regards, Peter
>>>>
>>>>>
>>>>> /Claes
>>>>>
>>>>> PS: The default list of types is generated in a few adhoc tests not
>>>>> part of this patch. I'm considering proposing add support for
>>>>> generating this list at build time. Maybe a JEP called "Build
>>>>> system support for profile-guided optimizations", which could also
>>>>> handle https://bugs.openjdk.java.net/browse/JDK-8150044
>>>>>
>>>>> On 2016-03-30 09:53, Peter Levart wrote:
>>>>>> Hi Claes,
>>>>>>
>>>>>> Sorry, I found a flaw in the benchmark (the regex pattern to split
>>>>>> comma-separated string into substrings was wrong). What the
>>>>>> benchmark did was compare the overheads of a single lookup of a
>>>>>> longer class name containing commas. Here's the corrected result
>>>>>> of overheads of 5 unsuccessful lookups:
>>>>>>
>>>>>> Benchmark (generate)                       (lookup)  Mode  Cnt
>>>>>> Score     Error  Units
>>>>>> ClassForNameBench.classForName LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10  29627.141 ± 567.427  ns/op
>>>>>> ClassForNameBench.classForNameInModule LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10   1073.256 ±  23.794  ns/op
>>>>>> ClassForNameBench.hashSetContains LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10     33.022 ±   0.066  ns/op
>>>>>> ClassForNameBench.switchStatement LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10     38.498 ±   5.842  ns/op
>>>>>>
>>>>>> ...overheads are a little bigger (x5 approx.).
>>>>>>
>>>>>>
>>>>>> Here's the corrected benchmark:
>>>>>>
>>>>>>
>>>>>> package jdk.test;
>>>>>>
>>>>>> import org.openjdk.jmh.annotations.*;
>>>>>> import org.openjdk.jmh.infra.Blackhole;
>>>>>>
>>>>>> import java.io.Serializable;
>>>>>> import java.lang.invoke.MethodHandle;
>>>>>> import java.util.Iterator;
>>>>>> import java.util.Set;
>>>>>> import java.util.concurrent.TimeUnit;
>>>>>> import java.util.stream.Collectors;
>>>>>> import java.util.stream.Stream;
>>>>>>
>>>>>> @BenchmarkMode(Mode.AverageTime)
>>>>>> @Fork(value = 1, warmups = 0)
>>>>>> @Warmup(iterations = 10)
>>>>>> @Measurement(iterations = 10)
>>>>>> @OutputTimeUnit(TimeUnit.NANOSECONDS)
>>>>>> @State(Scope.Thread)
>>>>>> public class ClassForNameBench {
>>>>>>
>>>>>>     static final String BMH = "java/lang/invoke/BoundMethodHandle";
>>>>>>     static final String SPECIES_PREFIX_NAME = "Species_";
>>>>>>     static final String SPECIES_PREFIX_PATH = BMH + "$" +
>>>>>> SPECIES_PREFIX_NAME;
>>>>>>
>>>>>>     @Param({"LL,LLL,LLLL,LLLLL,LLLLLL"})
>>>>>>     public String generate;
>>>>>>
>>>>>>     @Param({"LLI,LLLI,LLLLI,LLLLLI,LLLLLLI"})
>>>>>>     public String lookup;
>>>>>>
>>>>>>     private String[] generatedTypes;
>>>>>>     private String[] lookedUpTypes;
>>>>>>     private Set<String> generatedNames;
>>>>>>     private String[] lookedUpNames;
>>>>>>
>>>>>>     @Setup(Level.Trial)
>>>>>>     public void setup() {
>>>>>>         generatedTypes = generate.trim().split("\\s*,\\s*");
>>>>>>         lookedUpTypes = lookup.trim().split("\\s*,\\s*");
>>>>>>         generatedNames = Stream.of(generatedTypes)
>>>>>>                                .map(types -> SPECIES_PREFIX_PATH +
>>>>>> shortenSignature(types))
>>>>>> .collect(Collectors.toSet());
>>>>>>         lookedUpNames = Stream.of(lookedUpTypes)
>>>>>>                               .map(types -> SPECIES_PREFIX_PATH +
>>>>>> shortenSignature(types))
>>>>>>                               .toArray(String[]::new);
>>>>>>     }
>>>>>>
>>>>>>     @Benchmark
>>>>>>     public void classForName(Blackhole bh) {
>>>>>>         for (String name : lookedUpNames) {
>>>>>>             try {
>>>>>>                 bh.consume(Class.forName(name, false,
>>>>>> MethodHandle.class.getClassLoader()));
>>>>>>             } catch (ClassNotFoundException e) {
>>>>>>                 bh.consume(e);
>>>>>>             }
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>>     @Benchmark
>>>>>>     public void classForNameInModule(Blackhole bh) {
>>>>>>         for (String name : lookedUpNames) {
>>>>>> bh.consume(Class.forName(MethodHandle.class.getModule(), name));
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>>     @Benchmark
>>>>>>     public void hashSetContains(Blackhole bh) {
>>>>>>         for (String name : lookedUpNames) {
>>>>>>             bh.consume(generatedNames.contains(name));
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>>     @Benchmark
>>>>>>     public void switchStatement(Blackhole bh) {
>>>>>>         for (String types : lookedUpTypes) {
>>>>>>             bh.consume(getBMHSwitch(types));
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>>     static Class<?> getBMHSwitch(String types) {
>>>>>>         // should be in sync with @Param generate above...
>>>>>>         switch (types) {
>>>>>>             case "LL": return Object.class;
>>>>>>             case "LLL": return Serializable.class;
>>>>>>             case "LLLL": return Iterator.class;
>>>>>>             case "LLLLL": return Throwable.class;
>>>>>>             case "LLLLLL": return String.class;
>>>>>>             default: return null;
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>>     // copied from non-public LambdaForm
>>>>>>     static String shortenSignature(String signature) {
>>>>>>         // Hack to make signatures more readable when they show up
>>>>>> in method names.
>>>>>>         final int NO_CHAR = -1, MIN_RUN = 3;
>>>>>>         int c0, c1 = NO_CHAR, c1reps = 0;
>>>>>>         StringBuilder buf = null;
>>>>>>         int len = signature.length();
>>>>>>         if (len < MIN_RUN) return signature;
>>>>>>         for (int i = 0; i <= len; i++) {
>>>>>>             // shift in the next char:
>>>>>>             c0 = c1; c1 = (i == len ? NO_CHAR : signature.charAt(i));
>>>>>>             if (c1 == c0) { ++c1reps; continue; }
>>>>>>             // shift in the next count:
>>>>>>             int c0reps = c1reps; c1reps = 1;
>>>>>>             // end of a  character run
>>>>>>             if (c0reps < MIN_RUN) {
>>>>>>                 if (buf != null) {
>>>>>>                     while (--c0reps >= 0)
>>>>>>                         buf.append((char) c0);
>>>>>>                 }
>>>>>>                 continue;
>>>>>>             }
>>>>>>             // found three or more in a row
>>>>>>             if (buf == null)
>>>>>>                 buf = new StringBuilder().append(signature, 0, i -
>>>>>> c0reps);
>>>>>>             buf.append((char) c0).append(c0reps);
>>>>>>         }
>>>>>>         return (buf == null) ? signature : buf.toString();
>>>>>>     }
>>>>>> }
>>>>>>
>>>>>>
>>>>>> Regards, Peter
>>>>>>
>>>>>>
>>>>>>
>>>>>> On 03/30/2016 09:40 AM, Peter Levart wrote:
>>>>>>> Hi Claes,
>>>>>>>
>>>>>>> On 03/30/2016 01:03 AM, Claes Redestad wrote:
>>>>>>>> Hi Peter, Mandy,
>>>>>>>>
>>>>>>>> On 2016-03-26 12:47, Peter Levart wrote:
>>>>>>>>>
>>>>>>>>> Comparing this with proposed code from webrev:
>>>>>>>>>
>>>>>>>>>  493                         try {
>>>>>>>>>  494                             return (Class<? extends
>>>>>>>>> BoundMethodHandle>)
>>>>>>>>>  495
>>>>>>>>> Class.forName("java.lang.invoke.BoundMethodHandle$Species_" +
>>>>>>>>> LambdaForm.shortenSignature(types));
>>>>>>>>>  496                         } catch (ClassNotFoundException
>>>>>>>>> cnf) {
>>>>>>>>>  497                             // Not pregenerated, generate
>>>>>>>>> the class
>>>>>>>>>  498                             return
>>>>>>>>> generateConcreteBMHClass(types);
>>>>>>>>>  499                         }
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> ...note that the function passed to CLASS_CACHE.computeIfAbsent
>>>>>>>>> is executed just once per distinct 'types' argument. Even if
>>>>>>>>> you put the generated switch between a call to
>>>>>>>>> getConcreteBMHClass and CLASS_CACHE.computeIfAbsent,
>>>>>>>>> getConcreteBMHClass(String types) is executed just once per
>>>>>>>>> distinct 'types' argument (except in rare occasions when VM can
>>>>>>>>> not initialize the loaded class).
>>>>>>>>>
>>>>>>>>> In this respect a successful Class.forName() is not any worse
>>>>>>>>> than static resolving. It's just that unsuccessful
>>>>>>>>> Class.forName() represents some overhead for classes that are
>>>>>>>>> not pre-generated. So an alternative way to get rid of that
>>>>>>>>> overhead is to have a HashSet of 'types' strings for
>>>>>>>>> pre-generated classes at hand in order to decide whether to
>>>>>>>>> call Class.forName or generateConcreteBMHClass.
>>>>>>>>>
>>>>>>>>> What's easier to support is another question.
>>>>>>>>>
>>>>>>>>> Regards, Peter
>>>>>>>>
>>>>>>>> to have something to compare with I built a version which, like
>>>>>>>> you suggest,
>>>>>>>> generates a HashSet<String> with the set of generated classes here:
>>>>>>>>
>>>>>>>> http://cr.openjdk.java.net/~redestad/8152641/webrev.02/
>>>>>>>>
>>>>>>>> This adds a fair bit of complexity to the plugin and requires we
>>>>>>>> add a nested
>>>>>>>> class in BoundMethodHandle that we can replace. Using the anonymous
>>>>>>>> compute Function for this seems like the best choice for this.
>>>>>>>
>>>>>>> ...what I had in mind as alternative to a pregenerated class with
>>>>>>> a switch was a simple textual resource file, generated by plugin,
>>>>>>> read-in by BMH into a HashSet. No special-purpose class
>>>>>>> generation and much less complexity for the plugin.
>>>>>>>
>>>>>>>>
>>>>>>>> I've not been able to measure any statistical difference in real
>>>>>>>> startup terms,
>>>>>>>> though, and not figured out a smart way to benchmark the
>>>>>>>> overhead of the
>>>>>>>> CNFE in relation to the class generation (my guess it adds a
>>>>>>>> fraction to the
>>>>>>>> total cost) and since this adds ever so little footprint and an
>>>>>>>> extra lookup to the
>>>>>>>> fast path it would seem that this is the wrong trade-off to do
>>>>>>>> here.
>>>>>>>
>>>>>>> Yes, perhaps it would be best to just use Class.forName(module,
>>>>>>> className) instead. I have created a little benchmark to compare
>>>>>>> overheads (just overheads) of unsuccessful lookups for
>>>>>>> pregenerated classes (a situation where a BMH class is requested
>>>>>>> that has not been pregenerated) and here's the result for
>>>>>>> overhead of 5 unsuccessfull lookups:
>>>>>>>
>>>>>>> Benchmark (generate)                       (lookup)  Mode  Cnt
>>>>>>> Score     Error  Units
>>>>>>> ClassForNameBench.classForName LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10  6800.878 ± 421.424  ns/op
>>>>>>> ClassForNameBench.classForNameInModule LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10   209.574 ±   2.114  ns/op
>>>>>>> ClassForNameBench.hashSetContains LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10     6.813 ±   0.317  ns/op
>>>>>>> ClassForNameBench.switchStatement LL,LLL,LLLL,LLLLL,LLLLLL
>>>>>>> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt   10     6.601 ±   0.061  ns/op
>>>>>>>
>>>>>>> ...compared to runtime BMH class generation and loading this is
>>>>>>> really a very minor overhead. I would just use
>>>>>>> Class.forName(module, className) and reduce the complexity of the
>>>>>>> plugin.
>>>>>>>
>>>>>>> What do you think?
>>>>>>>
>>>>>>>
>>>>>>> Here's the benchmark:
>>>>>>>
>>>>>>> package jdk.test;
>>>>>>>
>>>>>>> import org.openjdk.jmh.annotations.*;
>>>>>>> import org.openjdk.jmh.infra.Blackhole;
>>>>>>>
>>>>>>> import java.io.Serializable;
>>>>>>> import java.lang.invoke.MethodHandle;
>>>>>>> import java.util.Iterator;
>>>>>>> import java.util.Set;
>>>>>>> import java.util.concurrent.TimeUnit;
>>>>>>> import java.util.stream.Collectors;
>>>>>>> import java.util.stream.Stream;
>>>>>>>
>>>>>>> @BenchmarkMode(Mode.AverageTime)
>>>>>>> @Fork(value = 1, warmups = 0)
>>>>>>> @Warmup(iterations = 10)
>>>>>>> @Measurement(iterations = 10)
>>>>>>> @OutputTimeUnit(TimeUnit.NANOSECONDS)
>>>>>>> @State(Scope.Thread)
>>>>>>> public class ClassForNameBench {
>>>>>>>
>>>>>>>     static final String BMH = "java/lang/invoke/BoundMethodHandle";
>>>>>>>     static final String SPECIES_PREFIX_NAME = "Species_";
>>>>>>>     static final String SPECIES_PREFIX_PATH = BMH + "$" +
>>>>>>> SPECIES_PREFIX_NAME;
>>>>>>>
>>>>>>>     @Param({"LL,LLL,LLLL,LLLLL,LLLLLL"})
>>>>>>>     public String generate;
>>>>>>>
>>>>>>>     @Param({"LLI,LLLI,LLLLI,LLLLLI,LLLLLLI"})
>>>>>>>     public String lookup;
>>>>>>>
>>>>>>>     private String[] generatedTypes;
>>>>>>>     private String[] lookedUpTypes;
>>>>>>>     private Set<String> generatedNames;
>>>>>>>     private String[] lookedUpNames;
>>>>>>>
>>>>>>>     @Setup(Level.Trial)
>>>>>>>     public void setup() {
>>>>>>>         generatedTypes = generate.trim().split("\\s+,\\s+");
>>>>>>>         lookedUpTypes = lookup.trim().split("\\s+,\\s+");
>>>>>>>         generatedNames = Stream.of(generatedTypes)
>>>>>>>                                .map(types -> SPECIES_PREFIX_PATH
>>>>>>> + shortenSignature(types))
>>>>>>> .collect(Collectors.toSet());
>>>>>>>         lookedUpNames = Stream.of(lookedUpTypes)
>>>>>>>                               .map(types -> SPECIES_PREFIX_PATH +
>>>>>>> shortenSignature(types))
>>>>>>>                               .toArray(String[]::new);
>>>>>>>     }
>>>>>>>
>>>>>>>     @Benchmark
>>>>>>>     public void classForName(Blackhole bh) {
>>>>>>>         for (String name : lookedUpNames) {
>>>>>>>             try {
>>>>>>>                 bh.consume(Class.forName(name, false,
>>>>>>> MethodHandle.class.getClassLoader()));
>>>>>>>             } catch (ClassNotFoundException e) {
>>>>>>>                 bh.consume(e);
>>>>>>>             }
>>>>>>>         }
>>>>>>>     }
>>>>>>>
>>>>>>>     @Benchmark
>>>>>>>     public void classForNameInModule(Blackhole bh) {
>>>>>>>         for (String name : lookedUpNames) {
>>>>>>> bh.consume(Class.forName(MethodHandle.class.getModule(), name));
>>>>>>>         }
>>>>>>>     }
>>>>>>>
>>>>>>>     @Benchmark
>>>>>>>     public void hashSetContains(Blackhole bh) {
>>>>>>>         for (String name : lookedUpNames) {
>>>>>>>             bh.consume(generatedNames.contains(name));
>>>>>>>         }
>>>>>>>     }
>>>>>>>
>>>>>>>     @Benchmark
>>>>>>>     public void switchStatement(Blackhole bh) {
>>>>>>>         for (String types : lookedUpTypes) {
>>>>>>>             bh.consume(getBMHSwitch(types));
>>>>>>>         }
>>>>>>>     }
>>>>>>>
>>>>>>>     static Class<?> getBMHSwitch(String types) {
>>>>>>>         // should be in sync with @Param generate above...
>>>>>>>         switch (types) {
>>>>>>>             case "LL": return Object.class;
>>>>>>>             case "LLL": return Serializable.class;
>>>>>>>             case "LLLL": return Iterator.class;
>>>>>>>             case "LLLLL": return Throwable.class;
>>>>>>>             case "LLLLLL": return String.class;
>>>>>>>             default: return null;
>>>>>>>         }
>>>>>>>     }
>>>>>>>
>>>>>>>     // copied from non-public LambdaForm
>>>>>>>     static String shortenSignature(String signature) {
>>>>>>>         // Hack to make signatures more readable when they show
>>>>>>> up in method names.
>>>>>>>         final int NO_CHAR = -1, MIN_RUN = 3;
>>>>>>>         int c0, c1 = NO_CHAR, c1reps = 0;
>>>>>>>         StringBuilder buf = null;
>>>>>>>         int len = signature.length();
>>>>>>>         if (len < MIN_RUN) return signature;
>>>>>>>         for (int i = 0; i <= len; i++) {
>>>>>>>             // shift in the next char:
>>>>>>>             c0 = c1; c1 = (i == len ? NO_CHAR :
>>>>>>> signature.charAt(i));
>>>>>>>             if (c1 == c0) { ++c1reps; continue; }
>>>>>>>             // shift in the next count:
>>>>>>>             int c0reps = c1reps; c1reps = 1;
>>>>>>>             // end of a  character run
>>>>>>>             if (c0reps < MIN_RUN) {
>>>>>>>                 if (buf != null) {
>>>>>>>                     while (--c0reps >= 0)
>>>>>>>                         buf.append((char) c0);
>>>>>>>                 }
>>>>>>>                 continue;
>>>>>>>             }
>>>>>>>             // found three or more in a row
>>>>>>>             if (buf == null)
>>>>>>>                 buf = new StringBuilder().append(signature, 0, i
>>>>>>> - c0reps);
>>>>>>>             buf.append((char) c0).append(c0reps);
>>>>>>>         }
>>>>>>>         return (buf == null) ? signature : buf.toString();
>>>>>>>     }
>>>>>>> }
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> Regards, Peter
>>>>>>>
>>>>>>>>
>>>>>>>> All-in-all I lean towards moving forward with the first, simpler
>>>>>>>> revision of this
>>>>>>>> patch[1] and then evaluate if webrev.02 or a
>>>>>>>> String-switch+static resolve
>>>>>>>> as a follow-up.
>>>>>>>>
>>>>>>>> A compromise would be to keep the SpeciesLookup class introduced
>>>>>>>> here
>>>>>>>> to allow removing all overhead in case the plugin is disabled.
>>>>>>>>
>>>>>>>> Mandy: I've not found any test (under jdk/test/tools/jlink or
>>>>>>>> elsewhere) which
>>>>>>>> has to be updated when adding a plugin like this.
>>>>>>>>
>>>>>>>> Thanks!
>>>>>>>>
>>>>>>>> /Claes
>>>>>>>>
>>>>>>>> [1] http://cr.openjdk.java.net/~redestad/8152641/webrev.01/
>>>>>>>
>>>>>>
>>>>>
>>>>
>>>
>>
>



More information about the core-libs-dev mailing list