[ 
https://issues.apache.org/jira/browse/IGNITE-29109?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18123849#comment-18123849
 ] 

Nikolay Izhikov commented on IGNITE-29109:
------------------------------------------

{code:java}
package repro;

import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.IntSupplier;

import com.sun.management.ThreadMXBean;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.Ignition;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgniteEx;
import 
org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;

/**
 * Reproducer: the binary marshaller rebuilds the class descriptor of {@code 
TreeMap} and
 * {@code TreeSet} on every write.
 * <p>
 * {@code BinaryContext} registers both classes with {@code 
registerBinarilizableSystemClass}, which
 * caches a descriptor built by {@code systemClassDescriptor(...)} with {@code 
registered = false}.
 * Every write then calls {@code registerClass} -> {@code registerDescriptor}, 
which builds a
 * registered copy with {@code makeRegistered()} (a new {@code 
BinaryClassDescriptor}, with reflective
 * method look-ups that throw {@code NoSuchMethodException}) and offers it with 
{@code putIfAbsent}.
 * The unregistered entry is already there, so the copy is dropped and the work 
repeats on the next
 * write.
 * <p>
 * {@code BinaryTreeMap}, the write replacement of {@code TreeMap}, is 
registered the same way.
 * <p>
 * The program measures one value class per collection type, first as Ignite 
ships, then after
 * replacing the three cached descriptors ({@code TreeMap}, {@code TreeSet}, 
{@code BinaryTreeMap})
 * with registered ones through reflection, which is what the proposed fix does.
 */
public final class TreeCollectionDescriptorRepro {

    private static final int ROUNDS = 5;

    private static final int OPS = 20_000;

    private static long sink;

    private static final ThreadMXBean THREADS = (ThreadMXBean) 
ManagementFactory.getThreadMXBean();

    /** A value with one field, written by Ignite in OBJECT mode. */
    public static final class Holder {

        private final Object field;

        Holder(Object field) {
            this.field = field;
        }
    }

    private TreeCollectionDescriptorRepro() {
    }

    /**
     * Runs the measurement on a single embedded node.
     *
     * @param args not used
     * @throws Exception if the node cannot be started or the internals cannot 
be reached
     */
    public static void main(String[] args) throws Exception {
        TcpDiscoveryVmIpFinder ipFinder = new 
TcpDiscoveryVmIpFinder().setAddresses(List.of("127.0.0.1:49500"));
        IgniteConfiguration cfg = new IgniteConfiguration()
                .setIgniteInstanceName("repro")
                .setDiscoverySpi(new 
TcpDiscoverySpi().setLocalAddress("127.0.0.1").setLocalPort(49500)
                        .setLocalPortRange(1).setIpFinder(ipFinder));
        try (Ignite ignite = Ignition.start(cfg)) {
            System.out.println("Ignite " + ignite.version() + ", Java " + 
Runtime.version()
                    + ", " + Runtime.getRuntime().availableProcessors() + " 
CPUs");
            IgniteCache<Integer, Holder> cache = 
ignite.getOrCreateCache("repro");

            printHeader();
            measureAll(ignite, cache, "as shipped");

            Map<Class<?>, Object> descByCls = descriptorCache(ignite);
            Class<?> binaryTreeMap = 
Class.forName("org.apache.ignite.internal.binary.BinaryTreeMap");
            for (Class<?> cls : List.of(TreeMap.class, TreeSet.class, 
binaryTreeMap)) {
                Object desc = descByCls.get(cls);
                Method registered = 
desc.getClass().getDeclaredMethod("registered");
                Method makeRegistered = 
desc.getClass().getDeclaredMethod("makeRegistered");
                registered.setAccessible(true);
                makeRegistered.setAccessible(true);
                System.out.println("cached descriptor of " + cls.getName() + ": 
registered=" + registered.invoke(desc));
                descByCls.put(cls, makeRegistered.invoke(desc));
            }
            measureAll(ignite, cache, "descriptors registered");
        }
    }

    private static void printHeader() {
        System.out.printf("%-24s %-28s %22s %22s %22s%n", "setting", "field 
value", "toBinary", "cache.put", "cache.get");
    }

    private static void measureAll(Ignite ignite, IgniteCache<Integer, Holder> 
cache, String setting) {
        TreeMap<String, Long> twelve = new TreeMap<>();
        for (int i = 0; i < 12; i++) {
            twelve.put("key-" + i, (long) i);
        }
        Object[][] cases = {
                {"empty HashMap", new HashMap<String, Long>()},
                {"empty ArrayList", new ArrayList<String>()},
                {"empty TreeMap", new TreeMap<String, Long>()},
                {"empty TreeSet", new TreeSet<String>()},
                {"TreeMap with 12 entries", twelve},
        };
        for (Object[] c : cases) {
            Holder holder = new Holder(c[1]);
            double[] toBinary = perOp(() -> 
System.identityHashCode(ignite.binary().toBinary(holder)));
            double[] put = perOp(() -> {
                cache.put(1, holder);
                return 1;
            });
            cache.put(1, holder);
            double[] get = perOp(() -> System.identityHashCode(cache.get(1)));
            System.out.printf("%-24s %-28s %8.0f ns %8.0f B %8.0f ns %8.0f B 
%8.0f ns %8.0f B%n", setting, c[0],
                    toBinary[0], toBinary[1], put[0], put[1], get[0], get[1]);
        }
    }

    /** @return {median nanoseconds per op, median bytes allocated on this 
thread per op}. */
    private static double[] perOp(IntSupplier op) {
        for (int i = 0; i < 2 * OPS; i++) {
            sink += op.getAsInt();
        }
        double[] nanos = new double[ROUNDS];
        double[] bytes = new double[ROUNDS];
        for (int r = 0; r < ROUNDS; r++) {
            long startBytes = THREADS.getCurrentThreadAllocatedBytes();
            long start = System.nanoTime();
            for (int i = 0; i < OPS; i++) {
                sink += op.getAsInt();
            }
            nanos[r] = (System.nanoTime() - start) / (double) OPS;
            bytes[r] = (THREADS.getCurrentThreadAllocatedBytes() - startBytes) 
/ (double) OPS;
        }
        Arrays.sort(nanos);
        Arrays.sort(bytes);
        return new double[] {nanos[ROUNDS / 2], bytes[ROUNDS / 2]};
    }

    @SuppressWarnings("unchecked")
    private static Map<Class<?>, Object> descriptorCache(Ignite ignite) throws 
Exception {
        Object binaryContext = ((CacheObjectBinaryProcessorImpl) ((IgniteEx) 
ignite).context().cacheObjects()).binaryContext();
        Field descByCls = 
binaryContext.getClass().getDeclaredField("descByCls");
        descByCls.setAccessible(true);
        return (Map<Class<?>, Object>) descByCls.get(binaryContext);
    }
}
{code}

> TreeMap/TreeSet class descriptor is rebuilt on every binary write since 
> 2.18.0 (regression of IGNITE-26061)
> -----------------------------------------------------------------------------------------------------------
>
>                 Key: IGNITE-29109
>                 URL: https://issues.apache.org/jira/browse/IGNITE-29109
>             Project: Ignite
>          Issue Type: Bug
>            Reporter: Nikolay Izhikov
>            Assignee: Nikolay Izhikov
>            Priority: Major
>              Labels: ise
>             Fix For: 2.19
>
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> h3. Summary
> Since 2.18.0 every binary write of a {{java.util.TreeMap}} or 
> {{java.util.TreeSet}} rebuilds its {{BinaryClassDescriptor}} by reflection. 
> An empty {{TreeMap}} costs ~25x more to marshal than in 2.17.0 (~6 µs vs 
> ~0.26 µs); {{cache.put}} is ~10x slower and {{cache.get}} of a {{TreeMap}} 
> value ~9x slower. {{HashMap}} and {{ArrayList}} are not affected. The compute 
> closure wrappers {{GridClosureProcessor.C1/C1MLA/C2/C2MLA/C4/C4MLA}} take the 
> same slow path. Reported on the dev list by a user; reproducer attached.
> h3. Measurements
> Reporter's reproducer (attached), one embedded server node, single thread, 
> median of 5 rounds of 20 000 ops after warm-up, JDK 17, 12 CPUs, ns/op:
> ||Field value||2.17.0 toBinary||2.18.0 toBinary||2.17.0 put||2.18.0 
> put||2.17.0 get||2.18.0 get||
> |empty HashMap|395|521|1602|2669|1142|1673|
> |empty ArrayList|289|292|865|957|472|516|
> |empty TreeMap|261|*6221*|985|*9416*|532|*4728*|
> |empty TreeSet|364|*5722*|990|*8563*|587|556|
> |TreeMap, 12 entries|1098|*6956*|1873|*10208*|1139|*5335*|
> h3. Root cause
> Introduced by IGNITE-26061 (commit 06a557b2e4, PR #12230).
> # {{BinaryContext}} constructor now calls 
> {{registerBinarilizableSystemClass}} for {{BinaryTreeMap}}, {{TreeMap}} and 
> {{TreeSet}}; {{GridClosureProcessor.onBinaryContextCreated}} does the same 
> for the six closure wrappers.
> # {{registerBinarilizableSystemClass}} puts {{systemClassDescriptor(...)}} 
> into {{descByCls}}. That descriptor is created with {{registered = false}}.
> # On every write {{registerClass}} sees {{!desc.registered()}} and calls 
> {{registerDescriptor}}, which builds a registered copy with 
> {{desc.makeRegistered()}} (full {{BinaryClassDescriptor}} constructor with 
> reflective look-ups) and offers it with {{descByCls.putIfAbsent(...)}}.
> # The unregistered entry is already present, so {{putIfAbsent}} returns it 
> and the registered copy is discarded. The next write repeats the work.
> Before IGNITE-26061 these classes were listed by name in 
> {{BINARYLIZABLE_SYS_CLSS}} and nothing was in {{descByCls}} before the first 
> write, so the first {{putIfAbsent}} cached the registered descriptor.
> h3. Fix
> Cache the descriptor in the registered state:
> {code:java}
> public void registerBinarilizableSystemClass(Class<?> cls) {
>     String clsName = cls.getName();
>     descByCls.put(cls, systemClassDescriptor(cls, clsName, new 
> BinaryReflectiveSerializer()).makeRegistered());
> }
> {code}
> With the fix the reproducer shows empty {{TreeMap}} at 453 ns toBinary / 1119 
> ns put / 580 ns get, i.e. back at 2.17.0 level. A regression test is added to 
> {{BinaryContextPredefinedTypesTest}} asserting that the cached descriptors of 
> these system classes are registered and that {{registerClass}} returns the 
> cached instance.
> h3. Reproducer
> {{treemap-descriptor-repro.zip}} (attached): {{mvn -q compile exec:exec}} 
> runs against 2.18.0, {{-Dignite.version=2.17.0}} for comparison. See its 
> README.md.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to