com.changingbits.Builder.java Source code

Java tutorial

Introduction

Here is the source code for com.changingbits.Builder.java

Source

package com.changingbits;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

/** Build a {@link LongRangeMultiSet}.  First, create this
 *  and pass the ranges.  Then, optionally, add "training
 *  data" by calling {@link #record} many times.  Finally,
 *  call {@link getMultiSet}. */

public final class Builder {

    private static final String COMPILED_TREE_CLASS = LongRangeMultiSet.class.getName() + "$Compiled";
    private static final Method LOOKUP_METHOD = Method.getMethod("int lookup(long, int[])");
    private static final Type LONG_RANGE_MULTI_SET_TYPE = Type.getType(LongRangeMultiSet.class);

    private static final String COMPILED_COUNTER_CLASS = BaseLongRangeCounter.class.getName() + "$Compiled";
    private static final Method ADD_METHOD = Method.getMethod("void add(long)");
    private static final Method GET_COUNTS_METHOD = Method.getMethod("int[] getCounts()");
    private static final Type LONG_RANGE_COUNTER_TYPE = Type.getType(LongRangeCounter.class);
    private static final Type BASE_LONG_RANGE_COUNTER_TYPE = Type.getType(BaseLongRangeCounter.class);
    private static final Type INT_ARRAY_TYPE = Type.getType("[I");

    private static final String COMPILED_COUNTER_CLASS2 = LongRangeCounter.class.getName() + "$Compiled";
    private static final Type COMPILED_COUNTER_CLASS2_TYPE = Type
            .getType(COMPILED_COUNTER_CLASS2.replace('.', '/'));

    private final List<LongRange> elementaryIntervals;
    private final LongRange[] ranges;

    private final long[] elementaryCounts;

    // Set in finish:
    private Node root;

    /** Create a builder, accepting the full range of longs
     * ({@code Long.MIN_VALUE} to {@code Long.MAX_VALUE}.
     *
     * @param ranges Ranges to match. */
    public Builder(LongRange[] ranges) {
        this(ranges, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    /** Create this, accepting the specified min/max range of
     *  all values.  In some cases, bounding the incoming range
     *  can result in faster code.  After creating this,
     *  you should optionally call {@link #record} multiple
     *  times (once per value you expect to encounter) so
     *  that the resulting code can be optimized for this
     *  data set, and finally call {@link #finish}.
     *
     *  @param ranges Ranges to match.
     *  @param hardMin The value passed to {@link
     *    LongRangeMultiSet#lookup} will never be less than this.
     *  @param hardMax The value passed to {@link
     *    LongRangeMultiSet#lookup} will never be greater
     *    than this. */
    public Builder(LongRange[] ranges, long hardMin, long hardMax) {

        this.ranges = ranges;

        // Compute the "elementary intervals" from the
        // incoming ranges:

        // Maps an endpoint to int flags; 1 = start of
        // interval, 2 = end of interval:
        Map<Long, Integer> endsMap = new HashMap<Long, Integer>();

        endsMap.put(hardMin, 1);
        endsMap.put(hardMax, 2);

        for (LongRange range : ranges) {
            Integer cur = endsMap.get(range.minIncl);
            if (range.minIncl < hardMin) {
                throw new IllegalArgumentException("range falls below hardMin");
            }
            if (range.maxIncl > hardMax) {
                throw new IllegalArgumentException("range falls above hardMax");
            }
            if (cur == null) {
                endsMap.put(range.minIncl, 1);
            } else {
                endsMap.put(range.minIncl, cur.intValue() | 1);
            }
            cur = endsMap.get(range.maxIncl);
            if (cur == null) {
                endsMap.put(range.maxIncl, 2);
            } else {
                endsMap.put(range.maxIncl, cur.intValue() | 2);
            }
        }

        List<Long> endsList = new ArrayList<Long>(endsMap.keySet());
        Collections.sort(endsList);
        //System.out.println("ends=" + endsMap);

        elementaryIntervals = new ArrayList<LongRange>();
        int upto0 = 1;
        long v = endsList.get(0);
        long prev;
        if (endsMap.get(v) == 3) {
            elementaryIntervals.add(new LongRange(null, v, true, v, true));
            prev = v + 1;
        } else {
            prev = v;
        }
        while (upto0 < endsList.size()) {
            v = endsList.get(upto0);
            int flags = endsMap.get(v);
            //System.out.println("  v=" + v + " flags=" + flags);
            if (flags == 3) {
                // This point is both an end and a start; we need to
                // separate it:
                elementaryIntervals.add(new LongRange(null, prev, true, v - 1, true));
                elementaryIntervals.add(new LongRange(null, v, true, v, true));
                prev = v + 1;
            } else if (flags == 1) {
                // This point is only the start of an interval;
                // attach it to next interval:
                if (v > prev) {
                    elementaryIntervals.add(new LongRange(null, prev, true, v - 1, true));
                }
                prev = v;
            } else {
                assert flags == 2;
                // This point is only the end of an interval; attach
                // it to last interval:
                elementaryIntervals.add(new LongRange(null, prev, true, v, true));
                prev = v + 1;
            }
            //System.out.println("    ints=" + elementaryIntervals);
            upto0++;
        }

        elementaryCounts = new long[elementaryIntervals.size()];
        //System.out.println("intervals: " + elementaryIntervals);
    }

    /** Call this many times, once per value in your
     *  "training data set"; the builder will use this to
     *  optimize the tree structure to minimize the
     *  computation required for each call to {@link
     *  #lookup}. */
    public void record(long v) {
        if (root != null) {
            throw new IllegalStateException("Builder is already finished");
        }

        // nocommit is this working correctly :)
        int size = elementaryIntervals.size();
        int lo = 0;
        int hi = size - 1;
        while (true) {
            int mid = (lo + hi) >>> 1;
            LongRange r = elementaryIntervals.get(mid);
            if (v < r.minIncl) {
                hi = mid - 1;
            } else if (v > r.maxIncl) {
                lo = mid + 1;
            } else {
                elementaryCounts[mid]++;
                return;
            }
        }
    }

    /** Recursively splits the elementaryIntervals into
     *  tree, balanced according to how many times each
     *  interval was seen. */
    private Node split(int startIndex, int endIndex, int[] leafUpto) {
        //System.out.println("split startIndex=" + startIndex + " endIndex=" + endIndex);
        Node left, right;

        int leaf;

        if (startIndex < endIndex - 1) {
            long sum = 0;
            for (int i = startIndex; i < endIndex; i++) {
                sum += elementaryCounts[i];
            }
            long halfSum = sum / 2;
            long bestDistance = Long.MAX_VALUE;
            int bestIndex = 0;
            sum = 0;
            for (int i = startIndex; i < endIndex - 1; i++) {
                sum += elementaryCounts[i];
                long distance = Math.abs(sum - halfSum);
                if (distance < bestDistance) {
                    bestDistance = distance;
                    bestIndex = i;
                }
            }
            //System.out.println("  bestIndex=" + bestIndex + " bestDistance=" + bestDistance + " sum=" + sum);

            left = split(startIndex, bestIndex + 1, leafUpto);
            right = split(bestIndex + 1, endIndex, leafUpto);
            leaf = -1;
        } else {
            left = right = null;
            leaf = leafUpto[0];
            leafUpto[0]++;
        }

        Node n = new Node(elementaryIntervals.get(startIndex).minIncl,
                elementaryIntervals.get(endIndex - 1).maxIncl, left, right, leaf);

        return n;
    }

    /** Makes perfect binary tree. */
    private Node binarySplit(int numLeaves) {
        if (numLeaves == 1) {
            return new Node(elementaryIntervals.get(0).minIncl, elementaryIntervals.get(0).maxIncl, null, null, 0);
        }
        //System.out.println("binarySplit numLeaves=" + numLeaves);
        int levelCount = 1;
        int level = 0;
        while (levelCount < numLeaves) {
            levelCount *= 2;
            level++;
        }

        Node[] curLevel = new Node[levelCount];

        // Fill in partial bottom layer first:
        int extra = 2 * numLeaves - levelCount;
        //System.out.println("  fill first extra=" + extra);
        for (int i = 0; i < extra; i++) {
            curLevel[i] = new Node(elementaryIntervals.get(i).minIncl, elementaryIntervals.get(i).maxIncl, null,
                    null, i);
        }

        // Fill in next layer above, next:
        //System.out.println("  parent count=" + (extra/2));
        for (int i = 0; i < extra / 2; i++) {
            Node left = curLevel[2 * i];
            Node right = curLevel[2 * i + 1];
            curLevel[i] = new Node(left.start, right.end, left, right, -1);
        }

        int upto = extra / 2;
        //System.out.println("  extra leaf count=" + (numLeaves-extra));
        for (int i = extra; i < numLeaves; i++) {
            curLevel[upto++] = new Node(elementaryIntervals.get(i).minIncl, elementaryIntervals.get(i).maxIncl,
                    null, null, i);
        }

        level -= 2;
        levelCount /= 4;
        while (level >= 0) {
            //System.out.println("  finish cycle level=" + level + " levelCount=" + levelCount);
            for (int i = 0; i < levelCount; i++) {
                Node left = curLevel[2 * i];
                Node right = curLevel[2 * i + 1];
                curLevel[i] = new Node(left.start, right.end, left, right, -1);
            }
            level--;
            levelCount /= 2;
        }

        return curLevel[0];
    }

    /** Build the {@link LongRangeMultiSet}, by calling
     *  {@code finish(true)}. */
    private void finish(boolean useArrayImpl) {
        if (root == null) {
            int numLeaves = elementaryIntervals.size();
            for (int i = 0; i < numLeaves; i++) {
                if (elementaryCounts[i] == 0) {
                    // This will create a balanced binary tree, if no
                    // training data was sent:
                    elementaryCounts[i] = 1;
                }
            }
            //System.out.println("COUNTS: " + Arrays.toString(elementaryCounts));

            Map<Node, List<Integer>> byNode = new HashMap<>();
            if (useArrayImpl) {
                root = binarySplit(numLeaves);
            } else {
                root = split(0, numLeaves, new int[1]);
            }
            for (int i = 0; i < ranges.length; i++) {
                addOutputs(root, i, ranges[i], byNode);
            }
            setHasOutputs(root, byNode);
            //System.out.println("ROOT:\n" + root);
        }
    }

    /** Recursively assigns range outputs to each node. */
    void addOutputs(Node node, int index, LongRange range, Map<Node, List<Integer>> byNode) {
        if (node.start >= range.minIncl && node.end <= range.maxIncl) {
            // Our range is fully included in the incoming
            // range; add to our output list:
            List<Integer> nodeOutputs = byNode.get(node);
            if (nodeOutputs == null) {
                nodeOutputs = new ArrayList<Integer>();
                byNode.put(node, nodeOutputs);
            }
            nodeOutputs.add(index);
        } else if (node.left != null) {
            assert node.right != null;
            // Recurse:
            addOutputs(node.left, index, range, byNode);
            addOutputs(node.right, index, range, byNode);
        }
    }

    void setHasOutputs(Node node, Map<Node, List<Integer>> byNode) {
        List<Integer> outputs = byNode.get(node);
        if (outputs != null) {
            node.outputs = new int[outputs.size()];
            for (int i = 0; i < outputs.size(); i++) {
                node.outputs[i] = outputs.get(i);
            }
            node.hasOutputs = true;
        } else {
            node.hasOutputs = false;
        }

        if (node.left != null) {
            setHasOutputs(node.left, byNode);
            setHasOutputs(node.right, byNode);
            node.hasOutputs |= node.left.hasOutputs;
            node.hasOutputs |= node.right.hasOutputs;
        }
    }

    private void buildRangeToLeaf(Node node, ArrayList<Integer> currentRanges,
            Map<Integer, List<Integer>> rangeToLeaf) {
        if (node.outputs != null) {
            for (int output : node.outputs) {
                currentRanges.add(output);
            }
        }
        if (node.left != null) {
            buildRangeToLeaf(node.left, currentRanges, rangeToLeaf);
            buildRangeToLeaf(node.right, currentRanges, rangeToLeaf);
        } else {
            // Leaf
            for (int range : currentRanges) {
                List<Integer> leaves = rangeToLeaf.get(range);
                if (leaves == null) {
                    leaves = new ArrayList<Integer>();
                    rangeToLeaf.put(range, leaves);
                }
                leaves.add(node.leafIndex);
            }
        }
        if (node.outputs != null) {
            currentRanges.subList(currentRanges.size() - node.outputs.length, currentRanges.size()).clear();
        }
    }

    /** Build a {@link LongRangeMultiSet} implementation to
     *  lookup intervals for a given point.
     *
     *  @param useAsm If true, the tree will be compiled to
     *  java bytecodes using the {@code asm} library; typically
     *  this results in a faster (~3X) implementation. */
    public LongRangeMultiSet getMultiSet(boolean useAsm, boolean useArrayImpl) {

        finish(useArrayImpl);

        if (useAsm) {
            StringBuilder sb = new StringBuilder();
            sb.append('\n');
            int count = 0;
            for (LongRange range : ranges) {
                sb.append("// range ");
                sb.append(count++);
                sb.append(": ");
                sb.append(range);
                sb.append('\n');
            }
            sb.append('\n');
            sb.append("int upto = 0;\n");
            buildJavaSource(root, 0, sb);
            String javaSource = sb.toString();
            //System.out.println("java: " + javaSource);

            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            classWriter.visit(Opcodes.V1_7,
                    Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC,
                    COMPILED_TREE_CLASS.replace('.', '/'), null, LONG_RANGE_MULTI_SET_TYPE.getInternalName(), null);
            classWriter.visitSource(javaSource, null);

            Method m = Method.getMethod("void <init> ()");
            GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, m, null,
                    null, classWriter);
            constructor.loadThis();
            constructor.loadArgs();
            constructor.invokeConstructor(LONG_RANGE_MULTI_SET_TYPE, m);
            constructor.returnValue();
            constructor.endMethod();

            GeneratorAdapter gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, LOOKUP_METHOD,
                    null, null, classWriter);
            //Label labelTop = new Label();
            //Label labelEnd = new Label();
            //gen.visitLabel(labelTop);
            int uptoLocal = gen.newLocal(Type.INT_TYPE);
            //System.out.println("uptoLocal=" + uptoLocal);
            // nocommit is this not needed!?
            //gen.visitLocalVariable("upto", "I", null, labelTop, labelEnd, uptoLocal);
            gen.push(0);
            gen.storeLocal(uptoLocal, Type.INT_TYPE);
            buildAsm(gen, root, uptoLocal);
            // Return upto:
            gen.loadLocal(uptoLocal, Type.INT_TYPE);
            gen.returnValue();
            //gen.visitLabel(labelEnd);
            gen.endMethod();
            classWriter.visitEnd();

            byte[] bytes = classWriter.toByteArray();

            // javap -c /x/tmp/my.class
            /*
            try {
              FileOutputStream fos = new FileOutputStream(new File("/x/tmp/my.class"));
              fos.write(bytes);
              fos.close();
            } catch (Exception e) {
              throw new RuntimeException(e);
            }
            */

            // nocommit allow changing the class loader
            Class<? extends LongRangeMultiSet> treeClass = new Loader(LongRangeMultiSet.class.getClassLoader())
                    .define(COMPILED_TREE_CLASS, classWriter.toByteArray());
            try {
                return treeClass.getConstructor().newInstance();
            } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
                    | InvocationTargetException e) {
                throw new RuntimeException(e);
            }

        } else if (useArrayImpl) {
            return new ArrayLongRangeMultiSet(root);
        } else {
            return new SimpleLongRangeMultiSet(root);
        }
    }

    private void buildAsm(GeneratorAdapter gen, Node node, int uptoLocal) {

        if (node.outputs != null) {
            //System.out.println("gen outputs=" + node.outputs);
            // Increment any range outputs at the current node:
            for (int range : node.outputs) {
                // Load arg 1 (the int[] answers):
                gen.loadArg(1);
                // Load the index we will store to
                gen.loadLocal(uptoLocal, Type.INT_TYPE);
                // The range value we will store:
                gen.push(range);
                // Store it
                gen.arrayStore(Type.INT_TYPE);
                // Increment our upto:
                gen.iinc(uptoLocal, 1);
            }
        }

        if (node.left != null && (node.left.hasOutputs || node.right.hasOutputs)) {
            assert node.left.end + 1 == node.right.start;
            if (node.left.hasOutputs && node.right.hasOutputs) {
                // Recurse on either left or right
                Label labelLeft = new Label();
                Label labelEnd = new Label();
                gen.loadArg(0);
                gen.push(node.left.end);

                gen.ifCmp(Type.LONG_TYPE, GeneratorAdapter.LE, labelLeft);
                buildAsm(gen, node.right, uptoLocal);
                gen.goTo(labelEnd);
                gen.visitLabel(labelLeft);
                buildAsm(gen, node.left, uptoLocal);
                gen.visitLabel(labelEnd);
            } else if (node.left.hasOutputs) {
                // Recurse only on left
                Label labelEnd = new Label();
                gen.loadArg(0);
                gen.push(node.left.end);

                gen.ifCmp(Type.LONG_TYPE, GeneratorAdapter.GT, labelEnd);
                buildAsm(gen, node.left, uptoLocal);
                gen.visitLabel(labelEnd);
            } else {
                // Recurse only on right
                Label labelEnd = new Label();
                gen.loadArg(0);
                gen.push(node.left.end);

                gen.ifCmp(Type.LONG_TYPE, GeneratorAdapter.LE, labelEnd);
                buildAsm(gen, node.right, uptoLocal);
                gen.visitLabel(labelEnd);
            }
        }
    }

    /** Increments counts as field members (count0, count1,
     *  ...) instead of a this.intArray[0], ... */
    private void buildCounterAsm2(GeneratorAdapter gen, Node node, boolean sawOutputs) {

        sawOutputs |= node.outputs != null;

        if (node.left != null) {
            assert node.left.end + 1 == node.right.start;
            // Recurse on either left or right
            Label labelLeft = new Label();
            Label labelEnd = new Label();
            gen.loadArg(0);
            gen.push(node.left.end);

            gen.ifCmp(Type.LONG_TYPE, GeneratorAdapter.LE, labelLeft);
            buildCounterAsm2(gen, node.right, sawOutputs);
            gen.goTo(labelEnd);
            gen.visitLabel(labelLeft);
            buildCounterAsm2(gen, node.left, sawOutputs);
            gen.visitLabel(labelEnd);
        } else if (sawOutputs) {
            // leaf: this.countN++
            gen.loadThis();
            gen.loadThis();
            gen.getField(COMPILED_COUNTER_CLASS2_TYPE, "count" + node.leafIndex, Type.INT_TYPE);
            gen.push(1);
            gen.visitInsn(Opcodes.IADD);
            gen.putField(COMPILED_COUNTER_CLASS2_TYPE, "count" + node.leafIndex, Type.INT_TYPE);
        }
    }

    private void buildCounterAsm(GeneratorAdapter gen, Node node, boolean sawOutputs) {

        sawOutputs |= node.outputs != null;

        if (node.left != null) {
            assert node.left.end + 1 == node.right.start;
            // Recurse on either left or right
            Label labelLeft = new Label();
            Label labelEnd = new Label();
            gen.loadArg(0);
            gen.push(node.left.end);

            gen.ifCmp(Type.LONG_TYPE, GeneratorAdapter.LE, labelLeft);
            buildCounterAsm(gen, node.right, sawOutputs);
            gen.goTo(labelEnd);
            gen.visitLabel(labelLeft);
            buildCounterAsm(gen, node.left, sawOutputs);
            gen.visitLabel(labelEnd);
        } else if (sawOutputs) {
            // leaf: elementaryCounts[node.leafIndex]++
            gen.loadThis();
            gen.getField(BASE_LONG_RANGE_COUNTER_TYPE, "elementaryCounts", INT_ARRAY_TYPE);
            gen.push(node.leafIndex);
            gen.dup2();
            gen.arrayLoad(Type.INT_TYPE);
            gen.push(1);
            gen.visitInsn(Opcodes.IADD);
            gen.arrayStore(Type.INT_TYPE);
        }
    }

    static void indent(StringBuilder sb, int depth) {
        for (int i = 0; i < depth; i++) {
            sb.append("  ");
        }
    }

    private void buildJavaSource(Node node, int depth, StringBuilder sb) {
        indent(sb, depth);
        sb.append("// node: " + node.start + " to " + node.end + "\n");
        if (node.outputs != null) {
            for (int range : node.outputs) {
                indent(sb, depth);
                sb.append("answers[upto++] = " + range + ";\n");
            }
        }

        if (node.left != null && (node.left.hasOutputs || node.right.hasOutputs)) {
            indent(sb, depth);
            if (node.left.hasOutputs) {
                sb.append("if (v <= " + node.left.end + ") {\n");
                buildJavaSource(node.left, depth + 1, sb);
                indent(sb, depth);
                if (node.right.hasOutputs) {
                    sb.append("} else {\n");
                    buildJavaSource(node.right, depth + 1, sb);
                    indent(sb, depth);
                    sb.append("}\n");
                } else {
                    sb.append("}\n");
                }
            } else {
                sb.append("if (v >= " + node.right.start + ") {\n");
                buildJavaSource(node.right, depth + 1, sb);
                indent(sb, depth);
                sb.append("}\n");
            }
        }
    }

    public LongRangeCounter getCounter(boolean useAsm) {
        finish(false);
        if (useAsm) {
            StringBuilder sb = new StringBuilder();
            sb.append('\n');
            int count = 0;
            for (LongRange range : ranges) {
                sb.append("// range ");
                sb.append(count++);
                sb.append(": ");
                sb.append(range);
                sb.append('\n');
            }
            sb.append('\n');
            buildJavaCounterSource(root, 0, sb, false);
            String javaSource = sb.toString();
            //System.out.println("javaSource:\n" + javaSource);

            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            classWriter.visit(Opcodes.V1_7,
                    Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC,
                    COMPILED_COUNTER_CLASS.replace('.', '/'), null, BASE_LONG_RANGE_COUNTER_TYPE.getInternalName(),
                    null);
            classWriter.visitSource(javaSource, null);
            Method m = Method.getMethod("void <init> (com.changingbits.Node, int, int)");
            GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, m, null,
                    null, classWriter);
            constructor.loadThis();
            constructor.loadArgs();
            constructor.invokeConstructor(Type.getType(BaseLongRangeCounter.class), m);
            constructor.returnValue();
            constructor.endMethod();

            GeneratorAdapter gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, ADD_METHOD,
                    null, null, classWriter);
            buildCounterAsm(gen, root, false);
            gen.returnValue();
            gen.endMethod();
            classWriter.visitEnd();

            byte[] bytes = classWriter.toByteArray();

            // javap -c /x/tmp/my.class
            /*
            try {
              FileOutputStream fos = new FileOutputStream(new File("/x/tmp/counter.class"));
              fos.write(bytes);
              fos.close();
            } catch (Exception e) {
              throw new RuntimeException(e);
            }
            */

            // nocommit allow changing the class loader
            Class<? extends LongRangeCounter> cl = new CounterLoader(LongRangeCounter.class.getClassLoader())
                    .define(COMPILED_COUNTER_CLASS, classWriter.toByteArray());
            try {
                return cl.getConstructor(Node.class, int.class, int.class).newInstance(root,
                        elementaryIntervals.size(), ranges.length);
            } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
                    | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        } else {
            return new SimpleLongRangeCounter(root, elementaryIntervals, ranges.length);
        }
    }

    private void buildJavaCounterSource(Node node, int depth, StringBuilder sb, boolean sawOutputs) {
        indent(sb, depth);
        sb.append("// node: " + node.start + " to " + node.end + "\n");

        sawOutputs |= node.outputs != null;

        if (node.left != null) {
            indent(sb, depth);
            sb.append("if (v <= " + node.left.end + ") {\n");
            buildJavaCounterSource(node.left, depth + 1, sb, sawOutputs);
            indent(sb, depth);
            sb.append("} else {\n");
            buildJavaCounterSource(node.right, depth + 1, sb, sawOutputs);
            indent(sb, depth);
            sb.append("}\n");
        } else if (sawOutputs) {
            // leaf: elementaryCounts[node.leafIndex]++
            indent(sb, depth);
            sb.append("elementaryCounts[");
            sb.append(node.leafIndex);
            sb.append("]++;\n");
        }
    }

    private void buildJavaCounter2Source(Node node, int depth, StringBuilder sb, boolean sawOutputs) {
        indent(sb, depth);
        sb.append("// node: " + node.start + " to " + node.end + "\n");

        sawOutputs |= node.outputs != null;

        if (node.left != null) {
            indent(sb, depth);
            sb.append("if (v <= " + node.left.end + ") {\n");
            buildJavaCounter2Source(node.left, depth + 1, sb, sawOutputs);
            indent(sb, depth);
            sb.append("} else {\n");
            buildJavaCounter2Source(node.right, depth + 1, sb, sawOutputs);
            indent(sb, depth);
            sb.append("}\n");
        } else if (sawOutputs) {
            // leaf: elementaryCounts[node.leafIndex]++
            indent(sb, depth);
            sb.append("count");
            sb.append(node.leafIndex);
            sb.append("++;\n");
        }
    }

    public LongRangeCounter getCounter2() {
        finish(false);

        // Maps each range to the leaf counts that contribute to it:
        Map<Integer, List<Integer>> rangeToLeaf = new HashMap<>();
        buildRangeToLeaf(root, new ArrayList<Integer>(), rangeToLeaf);

        StringBuilder sb = new StringBuilder();
        sb.append('\n');
        sb.append("public void add(long v) {\n");
        int count = 0;
        for (LongRange range : ranges) {
            sb.append("  // range ");
            sb.append(count++);
            sb.append(": ");
            sb.append(range);
            sb.append('\n');
        }

        buildJavaCounter2Source(root, 1, sb, false);

        sb.append("}\n\n");
        sb.append("public int[] getCounts() {\n");
        sb.append("  int[] counts = new int[");
        sb.append(ranges.length);
        sb.append("];\n");
        for (int range = 0; range < ranges.length; range++) {
            List<Integer> elements = rangeToLeaf.get(range);
            if (elements != null) {
                sb.append("  counts[");
                sb.append(range);
                sb.append("] = count");
                sb.append(elements.get(0));

                for (int i = 1; i < elements.size(); i++) {
                    sb.append(" + count");
                    sb.append(elements.get(i));
                }
                sb.append(";\n");
            }
        }
        sb.append("  return counts;\n}\n");

        String javaSource = sb.toString();
        //System.out.println("counter2 javaSource:\n" + javaSource);

        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        classWriter.visit(Opcodes.V1_7,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC,
                COMPILED_COUNTER_CLASS2.replace('.', '/'), null, LONG_RANGE_COUNTER_TYPE.getInternalName(), null);
        classWriter.visitSource(javaSource, null);

        // Define "int countN" members:
        int numLeaves = elementaryIntervals.size();
        for (int i = 0; i < numLeaves; i++) {
            classWriter.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC, "count" + i, "I", null, null);
        }

        // init:
        Method m = Method.getMethod("void <init> ()");
        GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, m, null,
                null, classWriter);
        // Init all counters to 0:
        for (int i = 0; i < numLeaves; i++) {
            constructor.loadThis();
            constructor.push(0);
            constructor.putField(COMPILED_COUNTER_CLASS2_TYPE, "count" + i, Type.INT_TYPE);
        }
        constructor.loadThis();
        constructor.invokeConstructor(LONG_RANGE_COUNTER_TYPE, m);
        constructor.returnValue();
        constructor.endMethod();

        // void add(long v):
        GeneratorAdapter gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, ADD_METHOD, null,
                null, classWriter);
        buildCounterAsm2(gen, root, false);
        gen.returnValue();
        gen.endMethod();

        // int[] getCounts():
        gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, GET_COUNTS_METHOD, null, null,
                classWriter);
        int countsLocal = gen.newLocal(INT_ARRAY_TYPE);
        gen.push(ranges.length);
        gen.newArray(Type.INT_TYPE);
        gen.storeLocal(countsLocal);

        for (int range = 0; range < ranges.length; range++) {
            List<Integer> elements = rangeToLeaf.get(range);
            if (elements != null) {
                gen.loadLocal(countsLocal);
                gen.push(range);

                gen.loadThis();
                gen.getField(COMPILED_COUNTER_CLASS2_TYPE, "count" + elements.get(0), Type.INT_TYPE);

                for (int i = 1; i < elements.size(); i++) {
                    gen.loadThis();
                    gen.getField(COMPILED_COUNTER_CLASS2_TYPE, "count" + elements.get(i), Type.INT_TYPE);
                    gen.visitInsn(Opcodes.IADD);
                }

                gen.arrayStore(Type.INT_TYPE);
            }
        }

        gen.loadLocal(countsLocal);
        gen.returnValue();
        gen.endMethod();

        classWriter.visitEnd();

        byte[] bytes = classWriter.toByteArray();

        // javap -c /x/tmp/my.class
        /*
        try {
          FileOutputStream fos = new FileOutputStream(new File("/x/tmp/counter2.class"));
          fos.write(bytes);
          fos.close();
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        */

        // nocommit allow changing the class loader
        Class<? extends LongRangeCounter> cl = new CounterLoader(LongRangeCounter.class.getClassLoader())
                .define(COMPILED_COUNTER_CLASS2, classWriter.toByteArray());
        try {
            return cl.getConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
                | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    static final class Loader extends ClassLoader {
        Loader(ClassLoader parent) {
            super(parent);
        }

        public Class<? extends LongRangeMultiSet> define(String className, byte[] bytecode) {
            return defineClass(className, bytecode, 0, bytecode.length).asSubclass(LongRangeMultiSet.class);
        }
    }

    static final class CounterLoader extends ClassLoader {
        CounterLoader(ClassLoader parent) {
            super(parent);
        }

        public Class<? extends LongRangeCounter> define(String className, byte[] bytecode) {
            return defineClass(className, bytecode, 0, bytecode.length).asSubclass(LongRangeCounter.class);
        }
    }
}