edu.mit.streamjit.impl.compiler2.Compiler2.java Source code

Java tutorial

Introduction

Here is the source code for edu.mit.streamjit.impl.compiler2.Compiler2.java

Source

/*
 * Copyright (c) 2013-2015 Massachusetts Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package edu.mit.streamjit.impl.compiler2;

import com.google.common.base.Function;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeResolver;
import com.google.common.reflect.TypeToken;
import edu.mit.streamjit.api.DuplicateSplitter;
import edu.mit.streamjit.api.IllegalStreamGraphException;
import edu.mit.streamjit.api.Input;
import edu.mit.streamjit.api.Joiner;
import edu.mit.streamjit.api.Output;
import edu.mit.streamjit.api.Rate;
import edu.mit.streamjit.api.RoundrobinJoiner;
import edu.mit.streamjit.api.RoundrobinSplitter;
import edu.mit.streamjit.api.Splitter;
import edu.mit.streamjit.api.StreamCompilationFailedException;
import edu.mit.streamjit.api.StreamCompiler;
import edu.mit.streamjit.api.WeightedRoundrobinJoiner;
import edu.mit.streamjit.api.WeightedRoundrobinSplitter;
import edu.mit.streamjit.api.Worker;
import edu.mit.streamjit.impl.blob.Blob;
import edu.mit.streamjit.impl.blob.Blob.Token;
import edu.mit.streamjit.impl.blob.Buffer;
import edu.mit.streamjit.impl.blob.DrainData;
import edu.mit.streamjit.impl.blob.PeekableBuffer;
import edu.mit.streamjit.impl.common.Configuration;
import edu.mit.streamjit.impl.common.Configuration.IntParameter;
import edu.mit.streamjit.impl.common.Configuration.SwitchParameter;
import edu.mit.streamjit.impl.common.InputBufferFactory;
import edu.mit.streamjit.impl.common.OutputBufferFactory;
import edu.mit.streamjit.impl.common.Workers;
import edu.mit.streamjit.impl.compiler.Schedule;
import edu.mit.streamjit.impl.compiler2.Compiler2BlobHost.DrainInstruction;
import edu.mit.streamjit.impl.compiler2.Compiler2BlobHost.ReadInstruction;
import edu.mit.streamjit.impl.compiler2.Compiler2BlobHost.WriteInstruction;
import edu.mit.streamjit.test.Benchmark;
import edu.mit.streamjit.test.Benchmarker;
import edu.mit.streamjit.test.apps.fmradio.FMRadio;
import edu.mit.streamjit.util.CollectionUtils;
import edu.mit.streamjit.util.GeneralBinarySearch;
import edu.mit.streamjit.util.bytecode.methodhandles.Combinators;
import edu.mit.streamjit.util.Pair;
import edu.mit.streamjit.util.ReflectionUtils;
import edu.mit.streamjit.util.bytecode.Module;
import edu.mit.streamjit.util.bytecode.ModuleClassLoader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * @author Jeffrey Bosboom <jbosboom@csail.mit.edu>
 * @since 9/22/2013
 */
public class Compiler2 {
    public static final ImmutableSet<Class<?>> REMOVABLE_WORKERS = ImmutableSet.<Class<?>>of(
            RoundrobinSplitter.class, WeightedRoundrobinSplitter.class, DuplicateSplitter.class,
            RoundrobinJoiner.class, WeightedRoundrobinJoiner.class);
    public static final ImmutableSet<IndexFunctionTransformer> INDEX_FUNCTION_TRANSFORMERS = ImmutableSet
            .<IndexFunctionTransformer>of(new IdentityIndexFunctionTransformer()
    //         new ArrayifyIndexFunctionTransformer(false),
    //         new ArrayifyIndexFunctionTransformer(true)
    );
    public static final RemovalStrategy REMOVAL_STRATEGY = new BitsetRemovalStrategy();
    public static final FusionStrategy FUSION_STRATEGY = new BitsetFusionStrategy();
    public static final UnboxingStrategy UNBOXING_STRATEGY = new BitsetUnboxingStrategy();
    public static final AllocationStrategy ALLOCATION_STRATEGY = new SubsetBiasAllocationStrategy(8);
    public static final StorageStrategy INTERNAL_STORAGE_STRATEGY = new TuneInternalStorageStrategy();
    public static final StorageStrategy EXTERNAL_STORAGE_STRATEGY = new TuneExternalStorageStrategy();
    public static final SwitchingStrategy SWITCHING_STRATEGY = SwitchingStrategy.tunePerWorker();
    private static final AtomicInteger PACKAGE_NUMBER = new AtomicInteger();
    private final ImmutableSet<Worker<?, ?>> workers;
    private final ImmutableSet<ActorArchetype> archetypes;
    private final NavigableSet<Actor> actors;
    private ImmutableSortedSet<ActorGroup> groups;
    private ImmutableSortedSet<WorkerActor> actorsToBeRemoved;
    private final Configuration config;
    private final int maxNumCores;
    private final DrainData initialState;
    /**
     * If the blob is the entire graph, this is the overall input; else null.
     */
    private final Input<?> overallInput;
    /**
     * If the blob is the entire graph, this is the overall output; else null.
     */
    private final Output<?> overallOutput;
    private Buffer overallInputBuffer, overallOutputBuffer;
    private ImmutableMap<Token, Buffer> precreatedBuffers;
    private final ImmutableMap<Token, ImmutableList<Object>> initialStateDataMap;
    private final Set<Storage> storage;
    private ImmutableMap<ActorGroup, Integer> externalSchedule;
    private final Module module = new Module();
    private final ModuleClassLoader classloader = new ModuleClassLoader(module);
    private final String packageName = "compiler" + PACKAGE_NUMBER.getAndIncrement();
    private ImmutableMap<ActorGroup, Integer> initSchedule;
    /**
     * For each token in the blob, the number of items live on that edge after
     * the init schedule, without regard to removals.  (We could recover this
     * information from Actor.inputSlots when we're creating drain instructions,
     * but storing it simplifies the code and permits asserting we didn't lose
     * any items.)
     */
    private ImmutableMap<Token, Integer> postInitLiveness;
    /**
     * ConcreteStorage instances used during initialization (bound into the
     * initialization code).
     */
    private ImmutableMap<Storage, ConcreteStorage> initStorage;
    /**
     * ConcreteStorage instances used during the steady-state (bound into the
     * steady-state code).
     */
    private ImmutableMap<Storage, ConcreteStorage> steadyStateStorage;
    /**
     * Code to run the initialization schedule.  (Initialization is
     * single-threaded.)
     */
    private MethodHandle initCode;
    /**
     * Code to run the steady state schedule.  The blob host takes care of
     * filling/flushing buffers, adjusting storage and the global barrier.
     */
    private ImmutableList<MethodHandle> steadyStateCode;
    private final List<ReadInstruction> initReadInstructions = new ArrayList<>();
    private final List<WriteInstruction> initWriteInstructions = new ArrayList<>();
    private final List<Runnable> migrationInstructions = new ArrayList<>();
    private final List<ReadInstruction> readInstructions = new ArrayList<>();
    private final List<WriteInstruction> writeInstructions = new ArrayList<>();
    private final List<DrainInstruction> drainInstructions = new ArrayList<>();

    public Compiler2(Set<Worker<?, ?>> workers, Configuration config, int maxNumCores, DrainData initialState,
            Input<?> input, Output<?> output) {
        this.workers = ImmutableSet.copyOf(workers);
        Map<Class<?>, ActorArchetype> archetypesBuilder = new HashMap<>();
        Map<Worker<?, ?>, WorkerActor> workerActors = new HashMap<>();
        for (Worker<?, ?> w : workers) {
            @SuppressWarnings("unchecked")
            Class<? extends Worker<?, ?>> wClass = (Class<? extends Worker<?, ?>>) w.getClass();
            if (archetypesBuilder.get(wClass) == null)
                archetypesBuilder.put(wClass, new ActorArchetype(wClass, module));
            WorkerActor actor = new WorkerActor(w, archetypesBuilder.get(wClass));
            workerActors.put(w, actor);
        }
        this.archetypes = ImmutableSet.copyOf(archetypesBuilder.values());

        Map<Token, TokenActor> tokenActors = new HashMap<>();
        Table<Actor, Actor, Storage> storageTable = HashBasedTable.create();
        int[] inputTokenId = new int[] { Integer.MIN_VALUE }, outputTokenId = new int[] { Integer.MAX_VALUE };
        for (WorkerActor a : workerActors.values())
            a.connect(ImmutableMap.copyOf(workerActors), tokenActors, storageTable, inputTokenId, outputTokenId);
        this.actors = new TreeSet<>();
        this.actors.addAll(workerActors.values());
        this.actors.addAll(tokenActors.values());
        this.storage = new HashSet<>(storageTable.values());

        this.config = config;
        this.maxNumCores = maxNumCores;
        this.initialState = initialState;
        ImmutableMap.Builder<Token, ImmutableList<Object>> initialStateDataMapBuilder = ImmutableMap.builder();
        if (initialState != null) {
            for (Table.Cell<Actor, Actor, Storage> cell : storageTable.cellSet()) {
                Token tok;
                if (cell.getRowKey() instanceof TokenActor)
                    tok = ((TokenActor) cell.getRowKey()).token();
                else if (cell.getColumnKey() instanceof TokenActor)
                    tok = ((TokenActor) cell.getColumnKey()).token();
                else
                    tok = new Token(((WorkerActor) cell.getRowKey()).worker(),
                            ((WorkerActor) cell.getColumnKey()).worker());
                ImmutableList<Object> data = initialState.getData(tok);
                if (data != null && !data.isEmpty()) {
                    initialStateDataMapBuilder.put(tok, data);
                    cell.getValue().initialData().add(Pair.make(data, IndexFunction.identity()));
                }
            }
        }
        this.initialStateDataMap = initialStateDataMapBuilder.build();
        this.overallInput = input;
        this.overallOutput = output;
    }

    public Blob compile() {
        findRemovals();
        fuse();
        schedule();

        //      identityRemoval();
        splitterRemoval();
        joinerRemoval();

        inferTypes();
        unbox();

        generateArchetypalCode();
        createBuffers();
        createInitCode();
        createSteadyStateCode();
        return instantiateBlob();
    }

    private void findRemovals() {
        ImmutableSortedSet.Builder<WorkerActor> builder = ImmutableSortedSet.naturalOrder();
        next_worker: for (WorkerActor a : Iterables.filter(actors, WorkerActor.class)) {
            if (!REMOVABLE_WORKERS.contains(a.worker().getClass()))
                continue;
            for (Storage s : a.outputs())
                if (!s.initialData().isEmpty())
                    continue next_worker;
            for (Storage s : a.inputs())
                if (!s.initialData().isEmpty())
                    continue next_worker;
            if (!REMOVAL_STRATEGY.remove(a, config))
                continue;
            builder.add(a);
        }
        this.actorsToBeRemoved = builder.build();
    }

    /**
     * Fuses actors into groups as directed by the configuration.
     */
    private void fuse() {
        List<ActorGroup> actorGroups = new ArrayList<>();
        for (Actor a : actors)
            actorGroups.add(ActorGroup.of(a));

        //Fuse as much as possible.
        just_fused: do {
            try_fuse: for (Iterator<ActorGroup> it = actorGroups.iterator(); it.hasNext();) {
                ActorGroup g = it.next();
                if (g.isTokenGroup())
                    continue try_fuse;
                for (ActorGroup pg : g.predecessorGroups())
                    if (pg.isTokenGroup())
                        continue try_fuse;
                if (g.isPeeking() || g.predecessorGroups().size() > 1)
                    continue try_fuse;
                for (Storage s : g.inputs())
                    if (!s.initialData().isEmpty())
                        continue try_fuse;

                //We are assuming FusionStrategies are all happy to work
                //group-by-group.  If later we want to make all decisions at
                //once, we'll refactor existing FusionStrategies to inherit from
                //a base class containing this loop.
                if (!FUSION_STRATEGY.fuseUpward(g, config))
                    continue try_fuse;

                ActorGroup gpred = Iterables.getOnlyElement(g.predecessorGroups());
                ActorGroup fusedGroup = ActorGroup.fuse(g, gpred);
                it.remove();
                actorGroups.remove(gpred);
                actorGroups.add(fusedGroup);
                continue just_fused;
            }
            break;
        } while (true);

        this.groups = ImmutableSortedSet.copyOf(actorGroups);

        Boolean reportFusion = (Boolean) config.getExtraData("reportFusion");
        if (reportFusion != null && reportFusion) {
            for (ActorGroup g : groups) {
                if (g.isTokenGroup())
                    continue;
                List<Integer> list = new ArrayList<>();
                for (Actor a : g.actors())
                    list.add(a.id());
                System.out.println(com.google.common.base.Joiner.on(' ').join(list));
            }
            System.out.flush();
            System.exit(0);
        }
    }

    /**
     * Computes each group's internal schedule and the external schedule.
     */
    private void schedule() {
        for (ActorGroup g : groups)
            internalSchedule(g);
        externalSchedule();
        initSchedule();
    }

    private void externalSchedule() {
        Schedule.Builder<ActorGroup> scheduleBuilder = Schedule.builder();
        scheduleBuilder.addAll(groups);
        for (ActorGroup g : groups) {
            for (Storage e : g.outputs()) {
                Actor upstream = Iterables.getOnlyElement(e.upstream());
                Actor downstream = Iterables.getOnlyElement(e.downstream());
                ActorGroup other = downstream.group();
                int upstreamAdjust = g.schedule().get(upstream);
                int downstreamAdjust = other.schedule().get(downstream);
                scheduleBuilder.connect(g, other).push(e.push().max() * upstreamAdjust)
                        .pop(e.pop().max() * downstreamAdjust).peek(e.peek().max() * downstreamAdjust)
                        .bufferExactly(0);
            }
        }
        int multiplier = config.getParameter("multiplier", IntParameter.class).getValue();
        scheduleBuilder.multiply(multiplier);
        try {
            externalSchedule = scheduleBuilder.build().getSchedule();
        } catch (Schedule.ScheduleException ex) {
            throw new StreamCompilationFailedException("couldn't find external schedule; mult = " + multiplier, ex);
        }
    }

    /**
     * Computes the internal schedule for the given group.
     */
    private void internalSchedule(ActorGroup g) {
        Schedule.Builder<Actor> scheduleBuilder = Schedule.builder();
        scheduleBuilder.addAll(g.actors());
        for (Actor a : g.actors())
            scheduleBuilder.executeAtLeast(a, 1);
        for (Storage s : g.internalEdges()) {
            scheduleBuilder
                    .connect(Iterables.getOnlyElement(s.upstream()), Iterables.getOnlyElement(s.downstream()))
                    .push(s.push().max()).pop(s.pop().max()).peek(s.peek().max()).bufferExactly(0);
        }

        try {
            Schedule<Actor> schedule = scheduleBuilder.build();
            g.setSchedule(schedule.getSchedule());
        } catch (Schedule2.ScheduleException ex) {
            throw new StreamCompilationFailedException(
                    "couldn't find internal schedule for group " + g + "\n" + scheduleBuilder.toString(), ex);
        }
    }

    private void initSchedule() {
        Schedule.Builder<ActorGroup> scheduleBuilder = Schedule.builder();
        scheduleBuilder.addAll(groups);
        for (Storage s : storage) {
            if (s.isInternal())
                continue;
            Actor upstream = Iterables.getOnlyElement(s.upstream()),
                    downstream = Iterables.getOnlyElement(s.downstream());
            int upstreamAdjust = upstream.group().schedule().get(upstream);
            int downstreamAdjust = downstream.group().schedule().get(downstream);
            int throughput, excessPeeks;
            //TODO: avoid double-buffering token groups here?
            if (actorsToBeRemoved.contains(downstream) && false)
                throughput = excessPeeks = 0;
            else {
                throughput = s.push().max() * upstreamAdjust * externalSchedule.get(upstream.group());
                excessPeeks = Math.max(s.peek().max() - s.pop().max(), 0);
            }
            int initialDataSize = Iterables.getOnlyElement(s.initialData(),
                    new Pair<>(ImmutableList.<Object>of(), (MethodHandle) null)).first.size();
            scheduleBuilder.connect(upstream.group(), downstream.group()).push(s.push().max() * upstreamAdjust)
                    .pop(s.pop().max() * downstreamAdjust).peek(s.peek().max() * downstreamAdjust)
                    .bufferAtLeast(throughput + excessPeeks - initialDataSize);
        }

        IntParameter initBufferingCostParam = config.getParameter("InitBufferingCost", IntParameter.class);
        int initBufferCost = initBufferingCostParam.getValue(),
                fireCost = initBufferingCostParam.getMax() - initBufferCost;
        scheduleBuilder.costs(fireCost, initBufferCost);
        try {
            Schedule<ActorGroup> schedule = scheduleBuilder.build();
            this.initSchedule = schedule.getSchedule();
        } catch (Schedule2.ScheduleException ex) {
            throw new StreamCompilationFailedException("couldn't find init schedule", ex);
        }

        ImmutableMap.Builder<Token, Integer> postInitLivenessBuilder = ImmutableMap.builder();
        for (Storage s : storage) {
            if (s.isInternal())
                continue;
            Actor upstream = Iterables.getOnlyElement(s.upstream()),
                    downstream = Iterables.getOnlyElement(s.downstream());
            int upstreamExecutions = upstream.group().schedule().get(upstream) * initSchedule.get(upstream.group());
            int downstreamExecutions = downstream.group().schedule().get(downstream)
                    * initSchedule.get(downstream.group());
            int liveItems = s.push().max() * upstreamExecutions - s.pop().max() * downstreamExecutions
                    + s.initialDataIndices().size();
            assert liveItems >= 0 : s;

            int index = downstream.inputs().indexOf(s);
            assert index != -1;
            Token token;
            if (downstream instanceof WorkerActor) {
                Worker<?, ?> w = ((WorkerActor) downstream).worker();
                token = downstream.id() == 0 ? Token.createOverallInputToken(w)
                        : new Token(Workers.getPredecessors(w).get(index), w);
            } else
                token = ((TokenActor) downstream).token();
            StorageSlotList inputSlots = downstream.inputSlots(index);
            inputSlots.ensureCapacity(liveItems);
            for (int i = 0; i < liveItems; ++i)
                inputSlots.add(token, i);
            postInitLivenessBuilder.put(token, liveItems);
        }
        this.postInitLiveness = postInitLivenessBuilder.build();

        int initScheduleSize = 0;
        for (int i : initSchedule.values())
            initScheduleSize += i;
        //      System.out.println("init schedule size "+initScheduleSize);
        int totalBuffering = 0;
        for (int i : postInitLiveness.values())
            totalBuffering += i;
        //      System.out.println("total items buffered "+totalBuffering);
    }

    private void splitterRemoval() {
        for (WorkerActor splitter : actorsToBeRemoved) {
            if (!(splitter.worker() instanceof Splitter))
                continue;
            List<IndexFunction> transfers = splitterTransferFunctions(splitter);
            Storage survivor = Iterables.getOnlyElement(splitter.inputs());
            //Remove all instances of splitter, not just the first.
            survivor.downstream().removeAll(ImmutableList.of(splitter));
            IndexFunction Sin = Iterables.getOnlyElement(splitter.inputIndexFunctions());
            StorageSlotList drainInfo = splitter.inputSlots(0);
            for (int i = 0; i < splitter.outputs().size(); ++i) {
                Storage victim = splitter.outputs().get(i);
                IndexFunction t = transfers.get(i);
                for (Actor a : victim.downstream()) {
                    List<Storage> inputs = a.inputs();
                    List<IndexFunction> inputIndices = a.inputIndexFunctions();
                    for (int j = 0; j < inputs.size(); ++j) {
                        StorageSlotList inputSlots = a.inputSlots(j);
                        if (inputs.get(j).equals(victim)) {
                            inputs.set(j, survivor);
                            survivor.downstream().add(a);
                            inputIndices.set(j, inputIndices.get(j).andThen(t));
                            if (splitter.push(i).max() > 0) {
                                IndexFunction idxFxn = a.inputIndexFunctions().get(j);
                                int bulkSize = GeneralBinarySearch
                                        .binarySearch(idx -> idxFxn.applyAsInt(idx) < drainInfo.size(), 0);
                                int[] bulk = getBulk(bulkSize);
                                idxFxn.applyBulk(bulk);
                                inputSlots.ensureCapacity(bulkSize);
                                for (int q : bulk) {
                                    inputSlots.add(drainInfo.get(q));
                                    drainInfo.setDuplicate(q);
                                }
                            }
                            inputIndices.set(j, inputIndices.get(j).andThen(Sin));
                        }
                    }
                }

                for (Pair<ImmutableList<Object>, IndexFunction> item : victim.initialData())
                    survivor.initialData().add(new Pair<>(item.first, item.second.andThen(t)));
                storage.remove(victim);
            }

            removeActor(splitter);
            assert consistency();
        }
    }

    /**
     * Returns transfer functions for the given splitter.
     *
     * A splitter has one transfer function for each output that maps logical
     * output indices to logical input indices (representing the splitter's
     * distribution pattern).
     * @param a an actor
     * @return transfer functions, or null
     */
    private List<IndexFunction> splitterTransferFunctions(WorkerActor a) {
        assert REMOVABLE_WORKERS.contains(a.worker().getClass()) : a.worker().getClass();
        if (a.worker() instanceof RoundrobinSplitter || a.worker() instanceof WeightedRoundrobinSplitter) {
            int[] weights = new int[a.outputs().size()];
            for (int i = 0; i < weights.length; ++i)
                weights[i] = a.push(i).max();
            return roundrobinTransferFunctions(weights);
        } else if (a.worker() instanceof DuplicateSplitter) {
            return Collections.nCopies(a.outputs().size(), IndexFunction.identity());
        } else
            throw new AssertionError();
    }

    private void joinerRemoval() {
        for (WorkerActor joiner : actorsToBeRemoved) {
            if (!(joiner.worker() instanceof Joiner))
                continue;
            List<IndexFunction> transfers = joinerTransferFunctions(joiner);
            Storage survivor = Iterables.getOnlyElement(joiner.outputs());
            //Remove all instances of joiner, not just the first.
            survivor.upstream().removeAll(ImmutableList.of(joiner));
            IndexFunction Jout = Iterables.getOnlyElement(joiner.outputIndexFunctions());
            for (int i = 0; i < joiner.inputs().size(); ++i) {
                Storage victim = joiner.inputs().get(i);
                IndexFunction t = transfers.get(i);
                IndexFunction t2 = t.andThen(Jout);
                for (Actor a : victim.upstream()) {
                    List<Storage> outputs = a.outputs();
                    List<IndexFunction> outputIndices = a.outputIndexFunctions();
                    for (int j = 0; j < outputs.size(); ++j)
                        if (outputs.get(j).equals(victim)) {
                            outputs.set(j, survivor);
                            outputIndices.set(j, outputIndices.get(j).andThen(t2));
                            survivor.upstream().add(a);
                        }
                }

                for (Pair<ImmutableList<Object>, IndexFunction> item : victim.initialData())
                    survivor.initialData().add(new Pair<>(item.first, item.second.andThen(t2)));
                storage.remove(victim);
            }

            //Linearize drain info from the joiner's inputs.
            int maxIdx = 0;
            for (int i = 0; i < joiner.inputs().size(); ++i) {
                IndexFunction t = transfers.get(i);
                StorageSlotList inputSlots = joiner.inputSlots(i);
                if (inputSlots.size() > 0)
                    maxIdx = Math.max(maxIdx, t.applyAsInt(inputSlots.size() - 1));
            }
            StorageSlotList linearizedInput = StorageSlotList.ofHoles(maxIdx + 1);
            for (int i = 0; i < joiner.inputs().size(); ++i) {
                IndexFunction t = transfers.get(i);
                StorageSlotList inputSlots = joiner.inputSlots(i);
                int[] bulk = getBulk(inputSlots.size());
                t.applyBulk(bulk);
                for (int idx = 0; idx < inputSlots.size(); ++idx)
                    linearizedInput.set(bulk[idx], inputSlots.get(idx));
                inputSlots.clear();
                inputSlots.trimToSize();
            }

            if (!linearizedInput.isEmpty()) {
                for (Actor a : survivor.downstream())
                    for (int j = 0; j < a.inputs().size(); ++j)
                        if (a.inputs().get(j).equals(survivor)) {
                            StorageSlotList inputSlots = a.inputSlots(j);
                            IndexFunction idxFxn = a.inputIndexFunctions().get(j);
                            int bulkSize = GeneralBinarySearch
                                    .binarySearch(idx -> idxFxn.applyAsInt(idx) < linearizedInput.size(), 0);
                            int[] bulk = getBulk(bulkSize);
                            idxFxn.applyBulk(bulk);
                            for (int q : bulk) {
                                StorageSlot slot = linearizedInput.get(q);
                                inputSlots.add(slot);
                                linearizedInput.setDuplicate(q);
                            }
                        }
            }

            //         System.out.println("removed "+joiner);
            removeActor(joiner);
            assert consistency();
        }
    }

    private List<IndexFunction> joinerTransferFunctions(WorkerActor a) {
        assert REMOVABLE_WORKERS.contains(a.worker().getClass()) : a.worker().getClass();
        if (a.worker() instanceof RoundrobinJoiner || a.worker() instanceof WeightedRoundrobinJoiner) {
            int[] weights = new int[a.inputs().size()];
            for (int i = 0; i < weights.length; ++i)
                weights[i] = a.pop(i).max();
            return roundrobinTransferFunctions(weights);
        } else
            throw new AssertionError();
    }

    private List<IndexFunction> roundrobinTransferFunctions(int[] weights) {
        int[] weightPrefixSum = new int[weights.length + 1];
        for (int i = 1; i < weightPrefixSum.length; ++i)
            weightPrefixSum[i] = weightPrefixSum[i - 1] + weights[i - 1];
        int N = weightPrefixSum[weightPrefixSum.length - 1];
        //t_x(i) = N(i/w[x]) + sum_0_x-1{w} + (i mod w[x])
        //where the first two terms select a "window" and the third is the
        //index into that window.
        ImmutableList.Builder<IndexFunction> transfer = ImmutableList.builder();
        for (int x = 0; x < weights.length; ++x) {
            //don't capture the array in the lambda
            int weight = weights[x], prefixSum = weightPrefixSum[x];
            transfer.add(new RoundrobinTransferIndexFunction(weight, prefixSum, N));
        }
        return transfer.build();
    }

    private static int _roundrobinTransferFunction(int weight, int prefixSum, int N, int i) {
        //assumes nonnegative indices
        return N * (i / weight) + prefixSum + (i % weight);
    }

    private static final class RoundrobinTransferIndexFunction implements IndexFunction {
        private final int weight, prefixSum, N;

        private RoundrobinTransferIndexFunction(int weight, int prefixSum, int N) {
            this.weight = weight;
            this.prefixSum = prefixSum;
            this.N = N;
        }

        @Override
        public int applyAsInt(int operand) {
            return _roundrobinTransferFunction(weight, prefixSum, N, operand);
        }

        @Override
        public void applyBulk(int[] bulk) {
            //TODO: is just a specialization point enough?
            for (int i = 0; i < bulk.length; ++i)
                //            bulk[i] = applyAsInt(bulk[i]);
                bulk[i] = _roundrobinTransferFunction(weight, prefixSum, N, bulk[i]);
        }
    }

    /**
     * Removes an Actor from this compiler's data structures.  The Actor should
     * already have been unlinked from the graph (no incoming edges); this takes
     * care of removing it from the actors set, its actor group (possibly
     * removing the group if it's now empty), and the schedule.
     * @param a the actor to remove
     */
    private void removeActor(Actor a) {
        assert actors.contains(a) : a;
        actors.remove(a);
        ActorGroup g = a.group();
        g.remove(a);
        if (g.actors().isEmpty()) {
            groups = ImmutableSortedSet.copyOf(Sets.difference(groups, ImmutableSet.of(g)));
            externalSchedule = ImmutableMap
                    .copyOf(Maps.difference(externalSchedule, ImmutableMap.of(g, 0)).entriesOnlyOnLeft());
            initSchedule = ImmutableMap
                    .copyOf(Maps.difference(initSchedule, ImmutableMap.of(g, 0)).entriesOnlyOnLeft());
        }
    }

    private boolean consistency() {
        Set<Storage> usedStorage = new HashSet<>();
        for (Actor a : actors) {
            usedStorage.addAll(a.inputs());
            usedStorage.addAll(a.outputs());
        }
        if (!storage.equals(usedStorage)) {
            Set<Storage> unused = Sets.difference(storage, usedStorage);
            Set<Storage> untracked = Sets.difference(usedStorage, storage);
            throw new AssertionError(
                    String.format("inconsistent storage:%n\tunused: %s%n\tuntracked:%s%n", unused, untracked));
        }
        return true;
    }

    //<editor-fold defaultstate="collapsed" desc="Unimplemented optimization stuff">
    //   /**
    //    * Removes Identity instances from the graph, unless doing so would make the
    //    * graph empty.
    //    */
    //   private void identityRemoval() {
    //      //TODO: remove from group, possibly removing the group if it becomes empty
    //      for (Iterator<Actor> iter = actors.iterator(); iter.hasNext();) {
    //         if (actors.size() == 1)
    //            break;
    //         Actor actor = iter.next();
    //         if (!actor.archetype().workerClass().equals(Identity.class))
    //            continue;
    //
    //         iter.remove();
    //         assert actor.predecessors().size() == 1 && actor.successors().size() == 1;
    //         Object upstream = actor.predecessors().get(0), downstream = actor.successors().get(0);
    //         if (upstream instanceof Actor)
    //            replace(((Actor)upstream).successors(), actor, downstream);
    //         if (downstream instanceof Actor)
    //            replace(((Actor)downstream).predecessors(), actor, upstream);
    //         //No index function changes required for Identity actors.
    //      }
    //   }
    //
    //   private static int replace(List<Object> list, Object target, Object replacement) {
    //      int replacements = 0;
    //      for (int i = 0; i < list.size(); ++i)
    //         if (Objects.equals(list.get(0), target)) {
    //            list.set(i, replacement);
    //            ++replacements;
    //         }
    //      return replacements;
    //   }
    //</editor-fold>

    /**
     * Performs type inference to replace type variables with concrete types.
     * For now, we only care about wrapper types.
     */
    public void inferTypes() {
        while (inferUpward() || inferDownward())
            ;
    }

    private boolean inferUpward() {
        boolean changed = false;
        //For each storage, if a reader's input type is a final type, all
        //writers' output types must be that final type.  (Wrappers are final,
        //so this works for wrappers, and maybe detects errors related to other
        //types.)
        for (Storage s : storage) {
            Set<TypeToken<?>> finalInputTypes = new HashSet<>();
            for (Actor a : s.downstream())
                if (Modifier.isFinal(a.inputType().getRawType().getModifiers()))
                    finalInputTypes.add(a.inputType());
            if (finalInputTypes.isEmpty())
                continue;
            if (finalInputTypes.size() > 1)
                throw new IllegalStreamGraphException("Type mismatch among readers: " + s.downstream());

            TypeToken<?> inputType = finalInputTypes.iterator().next();
            for (Actor a : s.upstream())
                if (!a.outputType().equals(inputType)) {
                    TypeToken<?> oldOutputType = a.outputType();
                    TypeResolver resolver = new TypeResolver().where(oldOutputType.getType(), inputType.getType());
                    TypeToken<?> newOutputType = TypeToken.of(resolver.resolveType(oldOutputType.getType()));
                    if (!oldOutputType.equals(newOutputType)) {
                        a.setOutputType(newOutputType);
                        //                  System.out.println("inferUpward: inferred "+a+" output type: "+oldOutputType+" -> "+newOutputType);
                        changed = true;
                    }

                    TypeToken<?> oldInputType = a.inputType();
                    TypeToken<?> newInputType = TypeToken.of(resolver.resolveType(oldInputType.getType()));
                    if (!oldInputType.equals(newInputType)) {
                        a.setInputType(newInputType);
                        //                  System.out.println("inferUpward: inferred "+a+" input type: "+oldInputType+" -> "+newInputType);
                        changed = true;
                    }
                }
        }
        return changed;
    }

    private boolean inferDownward() {
        boolean changed = false;
        //For each storage, find the most specific common type among all the
        //writers' output types, then if it's final, unify with any variable or
        //wildcard reader input type.  (We only unify if final to avoid removing
        //a type variable too soon.  We could also refine concrete types like
        //Object to a more specific subclass.)
        for (Storage s : storage) {
            Set<? extends TypeToken<?>> commonTypes = null;
            for (Actor a : s.upstream())
                if (commonTypes == null)
                    commonTypes = a.outputType().getTypes();
                else
                    commonTypes = Sets.intersection(commonTypes, a.outputType().getTypes());
            if (commonTypes.isEmpty())
                throw new IllegalStreamGraphException("No common type among writers: " + s.upstream());

            TypeToken<?> mostSpecificType = commonTypes.iterator().next();
            if (!Modifier.isFinal(mostSpecificType.getRawType().getModifiers()))
                continue;
            for (Actor a : s.downstream()) {
                TypeToken<?> oldInputType = a.inputType();
                //TODO: this isn't quite right?
                if (!ReflectionUtils.containsVariableOrWildcard(oldInputType.getType()))
                    continue;

                TypeResolver resolver = new TypeResolver().where(oldInputType.getType(),
                        mostSpecificType.getType());
                TypeToken<?> newInputType = TypeToken.of(resolver.resolveType(oldInputType.getType()));
                if (!oldInputType.equals(newInputType)) {
                    a.setInputType(newInputType);
                    //               System.out.println("inferDownward: inferred "+a+" input type: "+oldInputType+" -> "+newInputType);
                    changed = true;
                }

                TypeToken<?> oldOutputType = a.outputType();
                TypeToken<?> newOutputType = TypeToken.of(resolver.resolveType(oldOutputType.getType()));
                if (!oldOutputType.equals(newOutputType)) {
                    a.setOutputType(newOutputType);
                    //               System.out.println("inferDownward: inferred "+a+" output type: "+oldOutputType+" -> "+newOutputType);
                    changed = true;
                }
            }
        }
        return changed;
    }

    /**
     * Unboxes storage types and Actor input and output types.
     */
    private void unbox() {
        for (Storage s : storage) {
            if (isUnboxable(s.contentType()) && UNBOXING_STRATEGY.unboxStorage(s, config)) {
                TypeToken<?> contents = s.contentType();
                Class<?> type = contents.unwrap().getRawType();
                s.setType(type);
                //            if (!s.type().equals(contents.getRawType()))
                //               System.out.println("unboxed "+s+" to "+type);
            }
        }

        for (WorkerActor a : Iterables.filter(actors, WorkerActor.class)) {
            if (isUnboxable(a.inputType()) && UNBOXING_STRATEGY.unboxInput(a, config)) {
                TypeToken<?> oldType = a.inputType();
                a.setInputType(oldType.unwrap());
                //            if (!a.inputType().equals(oldType))
                //               System.out.println("unboxed input of "+a+": "+oldType+" -> "+a.inputType());
            }
            if (isUnboxable(a.outputType()) && UNBOXING_STRATEGY.unboxOutput(a, config)) {
                TypeToken<?> oldType = a.outputType();
                a.setOutputType(oldType.unwrap());
                //            if (!a.outputType().equals(oldType))
                //               System.out.println("unboxed output of "+a+": "+oldType+" -> "+a.outputType());
            }
        }
    }

    private boolean isUnboxable(TypeToken<?> type) {
        return Primitives.isWrapperType(type.getRawType()) && !type.getRawType().equals(Void.class);
    }

    private void generateArchetypalCode() {
        for (final ActorArchetype archetype : archetypes) {
            Iterable<WorkerActor> workerActors = FluentIterable.from(actors).filter(WorkerActor.class)
                    .filter(wa -> wa.archetype().equals(archetype));
            archetype.generateCode(packageName, classloader, workerActors);
            for (WorkerActor wa : workerActors)
                wa.setStateHolder(archetype.makeStateHolder(wa));
        }
    }

    /**
     * If we're compiling an entire graph, create the overall input and output
     * buffers now so we can take advantage of
     * PeekableBuffers/PokeableBuffers.  Otherwise we must pre-init()
     * our read/write instructions to refer to these buffers, since they won't
     * be passed to the blob's installBuffers().
     */
    private void createBuffers() {
        assert (overallInput == null) == (overallOutput == null);
        if (overallInput == null) {
            this.precreatedBuffers = ImmutableMap.of();
            return;
        }

        ActorGroup inputGroup = null, outputGroup = null;
        Token inputToken = null, outputToken = null;
        for (ActorGroup g : groups)
            if (g.isTokenGroup()) {
                assert g.actors().size() == 1;
                TokenActor ta = (TokenActor) g.actors().iterator().next();
                assert g.schedule().get(ta) == 1;
                if (ta.isInput()) {
                    assert inputGroup == null;
                    inputGroup = g;
                    inputToken = ta.token();
                }
                if (ta.isOutput()) {
                    assert outputGroup == null;
                    outputGroup = g;
                    outputToken = ta.token();
                }
            }
        this.overallInputBuffer = InputBufferFactory.unwrap(overallInput)
                .createReadableBuffer(Math.max(initSchedule.get(inputGroup), externalSchedule.get(inputGroup)));
        this.overallOutputBuffer = OutputBufferFactory.unwrap(overallOutput)
                .createWritableBuffer(Math.max(initSchedule.get(outputGroup), externalSchedule.get(outputGroup)));
        this.precreatedBuffers = ImmutableMap.<Token, Buffer>builder().put(inputToken, overallInputBuffer)
                .put(outputToken, overallOutputBuffer).build();
    }

    private void createInitCode() {
        ImmutableMap<Actor, ImmutableList<IndexFunction>> indexFxnBackup = adjustOutputIndexFunctions(
                Storage::initialDataIndices);

        this.initStorage = createStorage(false,
                new PeekPokeStorageFactory(InternalArrayConcreteStorage.initFactory(initSchedule)));
        initReadInstructions.add(new InitDataReadInstruction(initStorage, initialStateDataMap));

        ImmutableMap<Storage, ConcreteStorage> internalStorage = createStorage(true,
                InternalArrayConcreteStorage.initFactory(initSchedule));
        IndexFunctionTransformer ift = new IdentityIndexFunctionTransformer();
        ImmutableTable.Builder<Actor, Integer, IndexFunctionTransformer> inputTransformers = ImmutableTable
                .builder(), outputTransformers = ImmutableTable.builder();
        for (Actor a : Iterables.filter(actors, WorkerActor.class)) {
            for (int i = 0; i < a.inputs().size(); ++i)
                inputTransformers.put(a, i, ift);
            for (int i = 0; i < a.outputs().size(); ++i)
                outputTransformers.put(a, i, ift);
        }
        ImmutableMap.Builder<ActorGroup, Integer> unrollFactors = ImmutableMap.builder();
        for (ActorGroup g : groups)
            unrollFactors.put(g, 1);

        /**
         * During init, all (nontoken) groups are assigned to the same Core in
         * topological order (via the ordering on ActorGroups).  At the same
         * time we build the token init schedule information required by the
         * blob host.
         */
        Core initCore = new Core(CollectionUtils.union(initStorage, internalStorage),
                (table, wa) -> Combinators.lookupswitch(table), unrollFactors.build(), inputTransformers.build(),
                outputTransformers.build());
        for (ActorGroup g : groups)
            if (!g.isTokenGroup())
                initCore.allocate(g, Range.closedOpen(0, initSchedule.get(g)));
            else {
                assert g.actors().size() == 1;
                TokenActor ta = (TokenActor) g.actors().iterator().next();
                assert g.schedule().get(ta) == 1;
                ConcreteStorage storage = initStorage
                        .get(Iterables.getOnlyElement(ta.isInput() ? g.outputs() : g.inputs()));
                int executions = initSchedule.get(g);
                if (ta.isInput())
                    initReadInstructions.add(makeReadInstruction(ta, storage, executions));
                else
                    initWriteInstructions.add(makeWriteInstruction(ta, storage, executions));
            }
        this.initCode = initCore.code();

        restoreOutputIndexFunctions(indexFxnBackup);
    }

    private static final class AdditionIndexFunction implements IndexFunction {
        private final int addend;

        private AdditionIndexFunction(int addend) {
            this.addend = addend;
        }

        @Override
        public int applyAsInt(int operand) {
            return operand + addend;
        }

        @Override
        public void applyBulk(int[] bulk) {
            for (int i = 0; i < bulk.length; ++i)
                bulk[i] += addend;
        }
    }

    private void createSteadyStateCode() {
        for (Actor a : actors) {
            for (int i = 0; i < a.outputs().size(); ++i) {
                Storage s = a.outputs().get(i);

                if (s.isInternal())
                    continue;
                int itemsWritten = a.push(i).max() * initSchedule.get(a.group()) * a.group().schedule().get(a);
                a.outputIndexFunctions().set(i,
                        a.outputIndexFunctions().get(i).compose(new AdditionIndexFunction(itemsWritten)));
            }
            for (int i = 0; i < a.inputs().size(); ++i) {

                Storage s = a.inputs().get(i);
                if (s.isInternal())
                    continue;

                int itemsRead = a.pop(i).max() * initSchedule.get(a.group()) * a.group().schedule().get(a);
                a.inputIndexFunctions().set(i,
                        a.inputIndexFunctions().get(i).compose(new AdditionIndexFunction(itemsRead)));
            }
        }

        for (Storage s : storage)
            s.computeSteadyStateRequirements(externalSchedule);
        this.steadyStateStorage = createStorage(false,
                new PeekPokeStorageFactory(EXTERNAL_STORAGE_STRATEGY.asFactory(config)));

        ImmutableMap<Storage, ConcreteStorage> internalStorage = createStorage(true,
                INTERNAL_STORAGE_STRATEGY.asFactory(config));

        List<Core> ssCores = new ArrayList<>(maxNumCores);

        IndexFunctionTransformer ift = new IdentityIndexFunctionTransformer();
        for (int i = 0; i < maxNumCores; ++i) {

            ImmutableTable.Builder<Actor, Integer, IndexFunctionTransformer> inputTransformers = ImmutableTable
                    .builder(), outputTransformers = ImmutableTable.builder();
            for (Actor a : Iterables.filter(actors, WorkerActor.class)) {
                for (int j = 0; j < a.inputs().size(); ++j) {
                    inputTransformers.put(a, j, ift);
                }

                for (int j = 0; j < a.outputs().size(); ++j) {
                    outputTransformers.put(a, j, ift);
                }
            }

            ImmutableMap.Builder<ActorGroup, Integer> unrollFactors = ImmutableMap.builder();
            for (ActorGroup g : groups) {
                if (g.isTokenGroup())
                    continue;
                ////to be continued
                ////

            }

            ssCores.add(new Core(CollectionUtils.union(steadyStateStorage, internalStorage),
                    (table, wa) -> SWITCHING_STRATEGY.createSwitch(table, wa, config), unrollFactors.build(),
                    inputTransformers.build(), outputTransformers.build()));
        }

        int throughputPerSteadyState = 0;
        for (ActorGroup g : groups)
            if (!g.isTokenGroup())
                ALLOCATION_STRATEGY.allocateGroup(g, Range.closedOpen(0, externalSchedule.get(g)), ssCores, config);
            else {
                assert g.actors().size() == 1;
                TokenActor ta = (TokenActor) g.actors().iterator().next();
                assert g.schedule().get(ta) == 1;
                ConcreteStorage storage = steadyStateStorage
                        .get(Iterables.getOnlyElement(ta.isInput() ? g.outputs() : g.inputs()));
                int executions = externalSchedule.get(g);
                if (ta.isInput())
                    readInstructions.add(makeReadInstruction(ta, storage, executions));
                else {
                    writeInstructions.add(makeWriteInstruction(ta, storage, executions));
                    throughputPerSteadyState += executions;
                }
            }
        ImmutableList.Builder<MethodHandle> steadyStateCodeBuilder = ImmutableList.builder();
        for (Core c : ssCores)
            if (!c.isEmpty())
                steadyStateCodeBuilder.add(c.code());
        //Provide at least one core of code, even if it doesn't do anything; the
        //blob host will still copy inputs to outputs.
        this.steadyStateCode = steadyStateCodeBuilder.build();
        if (steadyStateCode.isEmpty())
            this.steadyStateCode = ImmutableList.of(Combinators.nop());

        createMigrationInstructions();
        createDrainInstructions();

        Boolean reportThroughput = (Boolean) config.getExtraData("reportThroughput");
        if (reportThroughput != null && reportThroughput) {
            ReportThroughputInstruction rti = new ReportThroughputInstruction(throughputPerSteadyState);
            readInstructions.add(rti);
            writeInstructions.add(rti);
        }
    }

    private ReadInstruction makeReadInstruction(TokenActor a, ConcreteStorage cs, int count) {
        assert a.isInput();
        Storage s = Iterables.getOnlyElement(a.outputs());
        IndexFunction idxFxn = Iterables.getOnlyElement(a.outputIndexFunctions());
        ReadInstruction retval;
        if (count == 0)
            retval = new NopReadInstruction(a.token());
        else if (cs instanceof PeekableBufferConcreteStorage)
            retval = new PeekReadInstruction(a, count);
        else if (s.type().isPrimitive() != true && cs instanceof BulkWritableConcreteStorage
                && contiguouslyIncreasing(idxFxn, 0, count)) {
            retval = new BulkReadInstruction(a, (BulkWritableConcreteStorage) cs, count);
        } else
            retval = new TokenReadInstruction(a, cs, count);
        retval.init(precreatedBuffers);
        return retval;
    }

    private WriteInstruction makeWriteInstruction(TokenActor a, ConcreteStorage cs, int count) {
        assert a.isOutput();
        Storage s = Iterables.getOnlyElement(a.inputs());
        IndexFunction idxFxn = Iterables.getOnlyElement(a.inputIndexFunctions());
        WriteInstruction retval;
        if (count == 0)
            retval = new NopWriteInstruction(a.token());
        else if (!s.type().isPrimitive() && cs instanceof BulkReadableConcreteStorage
                && contiguouslyIncreasing(idxFxn, 0, count)) {
            retval = new BulkWriteInstruction(a, (BulkReadableConcreteStorage) cs, count);
        } else
            retval = new TokenWriteInstruction(a, cs, count);
        retval.init(precreatedBuffers);
        return retval;
    }

    private boolean contiguouslyIncreasing(IndexFunction idxFxn, int start, int count) {
        try {
            int prev = idxFxn.applyAsInt(start);
            for (int i = start + 1; i < count; ++i) {
                int next = idxFxn.applyAsInt(i);
                if (next != (prev + 1))
                    return false;
                prev = next;
            }
            return true;
        } catch (Throwable ex) {
            throw new AssertionError("index functions should not throw", ex);
        }
    }

    /**
     * Create migration instructions: Runnables that move live items from
     * initialization to steady-state storage.
     */
    private void createMigrationInstructions() {
        for (Storage s : initStorage.keySet()) {
            ConcreteStorage init = initStorage.get(s), steady = steadyStateStorage.get(s);
            if (steady instanceof PeekableBufferConcreteStorage)
                migrationInstructions.add(new PeekMigrationInstruction(s, (PeekableBufferConcreteStorage) steady));
            else
                migrationInstructions.add(new MigrationInstruction(s, init, steady, this));
        }
    }

    /**
     * Create drain instructions, which collect live items from steady-state
     * storage when draining.
     */
    private void createDrainInstructions() {
        Map<Token, Pair<List<ConcreteStorage>, List<Integer>>> drainReads = new HashMap<>();
        for (Map.Entry<Token, Integer> e : postInitLiveness.entrySet())
            drainReads.put(e.getKey(), Pair.make(new ArrayList<>(Collections.nCopies(e.getValue(), null)),
                    Ints.asList(new int[e.getValue()])));

        for (Actor a : actors) {
            for (int input = 0; input < a.inputs().size(); ++input) {
                ConcreteStorage storage = steadyStateStorage.get(a.inputs().get(input));
                StorageSlotList inputSlots = a.inputSlots(input);
                int[] bulk = getBulk(inputSlots.size());
                a.inputIndexFunctions().get(input).applyBulk(bulk);
                for (int index = 0; index < inputSlots.size(); ++index) {
                    if (inputSlots.isDrainable(index)) {
                        Pair<List<ConcreteStorage>, List<Integer>> dr = drainReads.get(inputSlots.getToken(index));
                        int slotIndex = inputSlots.getIndex(index);
                        ConcreteStorage old = dr.first.set(slotIndex, storage);
                        assert old == null : "overwriting " + inputSlots.get(index);
                        dr.second.set(slotIndex, bulk[index]);
                    }
                }
            }
        }

        for (Map.Entry<Token, Pair<List<ConcreteStorage>, List<Integer>>> e : drainReads.entrySet()) {
            List<ConcreteStorage> storages = e.getValue().first;
            List<Integer> indices = e.getValue().second;
            assert !storages.contains(null) : "lost an element from " + e.getKey() + ": " + e.getValue();
            if (!storages.isEmpty() && storages.get(0) instanceof PeekableBufferConcreteStorage) {
                assert storages.stream()
                        .allMatch(s -> s instanceof PeekableBufferConcreteStorage) : "mixed peeking and not? "
                                + e.getKey() + ": " + e.getValue();
                //we still create a drain instruction, but it does nothing
                storages.clear();
                indices = Ints.asList();
            } else
                assert storages.stream()
                        .noneMatch(s -> s instanceof PeekableBufferConcreteStorage) : "mixed peeking and not? "
                                + e.getKey() + ": " + e.getValue();
            drainInstructions.add(new XDrainInstruction(e.getKey(), storages, indices));
        }

        for (WorkerActor wa : Iterables.filter(actors, WorkerActor.class))
            drainInstructions.add(wa.stateHolder());
    }

    //<editor-fold defaultstate="collapsed" desc="Output index function adjust/restore">
    /**
     * Adjust output index functions to avoid overwriting items in external
     * storage.  For any actor writing to external storage, we find the
     * first item that doesn't hit the live index set and add that many
     * (making that logical item 0 for writers).
     * @param liveIndexExtractor a function that computes the relevant live
     * index set for the given external storage
     * @return the old output index functions, to be restored later
     */
    private ImmutableMap<Actor, ImmutableList<IndexFunction>> adjustOutputIndexFunctions(
            Function<Storage, Set<Integer>> liveIndexExtractor) {
        ImmutableMap.Builder<Actor, ImmutableList<IndexFunction>> backup = ImmutableMap.builder();
        for (Actor a : actors) {
            backup.put(a, ImmutableList.copyOf(a.outputIndexFunctions()));
            for (int i = 0; i < a.outputs().size(); ++i) {
                if (a.push(i).max() == 0)
                    continue; //No writes -- nothing to adjust.
                Storage s = a.outputs().get(i);
                if (s.isInternal())
                    continue;
                Set<Integer> liveIndices = liveIndexExtractor.apply(s);
                assert liveIndices != null : s + " " + liveIndexExtractor;
                IndexFunction idxFxn = a.outputIndexFunctions().get(i);
                int offset = GeneralBinarySearch.binarySearch(o -> liveIndices.contains(idxFxn.applyAsInt(o)), 0);
                //Check future indices are also open (e.g., that we aren't
                //alternating hole/not-hole).
                for (int check = 0; check < 100; ++check)
                    assert !liveIndices.contains(a.translateOutputIndex(i, offset + check)) : check;
                final int finalOffset = offset;
                a.outputIndexFunctions().set(i,
                        a.outputIndexFunctions().get(i).compose(new AdditionIndexFunction(finalOffset)));
            }
        }
        return backup.build();
    }

    /**
     * Restores output index functions from a backup returned from
     * {@link #adjustOutputIndexFunctions(com.google.common.base.Function)}.
     * @param backup the backup to restore
     */
    private void restoreOutputIndexFunctions(ImmutableMap<Actor, ImmutableList<IndexFunction>> backup) {
        for (Actor a : actors) {
            ImmutableList<IndexFunction> oldFxns = backup.get(a);
            assert oldFxns != null : "no backup for " + a;
            assert oldFxns.size() == a.outputIndexFunctions().size() : "backup for " + a + " is wrong size";
            Collections.copy(a.outputIndexFunctions(), oldFxns);
        }
    }
    //</editor-fold>

    private ImmutableMap<Storage, ConcreteStorage> createStorage(boolean internal, StorageFactory factory) {
        ImmutableMap.Builder<Storage, ConcreteStorage> builder = ImmutableMap.builder();
        for (Storage s : storage)
            if (s.isInternal() == internal)
                builder.put(s, factory.make(s));
        return builder.build();
    }

    /**
     * Creates special ConcreteStorage implementations for PeekableBuffer and
     * PokeableBuffer, or falls back to the given factory.
     */
    private final class PeekPokeStorageFactory implements StorageFactory {
        private final StorageFactory fallback;
        private final boolean usePeekableBuffer;

        private PeekPokeStorageFactory(StorageFactory fallback) {
            this.fallback = fallback;
            SwitchParameter<Boolean> usePeekableBufferParam = config.getParameter("UsePeekableBuffer",
                    SwitchParameter.class, Boolean.class);
            this.usePeekableBuffer = usePeekableBufferParam.getValue();
        }

        @Override
        public ConcreteStorage make(Storage storage) {
            if (storage.id().isOverallInput() && overallInputBuffer instanceof PeekableBuffer && usePeekableBuffer)
                return PeekableBufferConcreteStorage
                        .factory(ImmutableMap.of(storage.id(), (PeekableBuffer) overallInputBuffer)).make(storage);
            //TODO: PokeableBuffer
            return fallback.make(storage);
        }
    }

    private static final class MigrationInstruction implements Runnable {
        private final ConcreteStorage init, steady;
        private final int[] indicesToMigrate;

        private MigrationInstruction(Storage storage, ConcreteStorage init, ConcreteStorage steady,
                Compiler2 compiler) {
            this.init = init;
            this.steady = steady;
            Set<Integer> builder = new HashSet<>();
            for (Actor a : storage.downstream())
                for (int i = 0; i < a.inputs().size(); ++i)
                    if (a.inputs().get(i).equals(storage)) {
                        StorageSlotList inputSlots = a.inputSlots(i);
                        int[] bulk = compiler.getBulk(inputSlots.size());
                        a.inputIndexFunctions().get(i).applyBulk(bulk);
                        for (int idx = 0; idx < inputSlots.size(); ++idx)
                            if (inputSlots.isLive(idx))
                                builder.add(bulk[idx]);
                    }
            this.indicesToMigrate = Ints.toArray(builder);
            //TODO: we used to sort here (using ImmutableSortedSet).  Does it matter?
        }

        @Override
        public void run() {
            init.sync();
            for (int i : indicesToMigrate)
                steady.write(i, init.read(i));
            steady.sync();
        }
    }

    private static final class PeekMigrationInstruction implements Runnable {
        private final PeekableBuffer buffer;
        private final int itemsConsumedDuringInit;

        private PeekMigrationInstruction(Storage storage, PeekableBufferConcreteStorage steady) {
            this.buffer = steady.buffer();
            this.itemsConsumedDuringInit = steady.minReadIndex();
        }

        @Override
        public void run() {
            buffer.consume(itemsConsumedDuringInit);
        }
    }

    /**
     * The X doesn't stand for anything.  I just needed a different name.
     */
    private static final class XDrainInstruction implements DrainInstruction {
        private final Token token;
        private final ConcreteStorage[] storage;
        private final int[] storageSelector, index;

        private XDrainInstruction(Token token, List<ConcreteStorage> storages, List<Integer> indices) {
            assert storages.size() == indices.size() : String.format("%s %s %s", token, storages, indices);
            this.token = token;
            Set<ConcreteStorage> set = new HashSet<>(storages);
            this.storage = set.toArray(new ConcreteStorage[set.size()]);
            this.storageSelector = new int[indices.size()];
            this.index = Ints.toArray(indices);
            for (int i = 0; i < indices.size(); ++i)
                storageSelector[i] = Arrays.asList(storage).indexOf(storages.get(i));
        }

        @Override
        public Map<Token, Object[]> call() {
            Object[] data = new Object[index.length];
            int idx = 0;
            for (int i = 0; i < index.length; ++i)
                data[idx++] = storage[storageSelector[i]].read(index[i]);
            return ImmutableMap.of(token, data);
        }
    }

    /**
     * PeekReadInstruction implements special handling for PeekableBuffer.
     */
    private static final class PeekReadInstruction implements ReadInstruction {
        private final Token token;
        private final int count;
        private PeekableBuffer buffer;

        private PeekReadInstruction(TokenActor a, int count) {
            assert a.isInput() : a;
            this.token = a.token();
            //If we "read" count new items, the maximum available index is given
            //by the output index function.
            this.count = a.translateOutputIndex(0, count);
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
            if (!buffers.containsKey(token))
                return;
            if (buffer != null)
                checkState(buffers.get(token) == buffer, "reassigning %s from %s to %s", token, buffer,
                        buffers.get(token));
            this.buffer = (PeekableBuffer) buffers.get(token);
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            //This shouldn't matter because we've already created the buffers.
            return ImmutableMap.of(token, count);
        }

        @Override
        public boolean load() {
            //Ensure data is present for reading.
            return buffer.size() >= count;
        }

        @Override
        public Map<Token, Object[]> unload() {
            //Data is not consumed from the underlying buffer until it's
            //adjusted, so no work is necessary here.
            return ImmutableMap.of();
        }
    }

    private static final class BulkReadInstruction implements ReadInstruction {
        private final Token token;
        private final BulkWritableConcreteStorage storage;
        private final int index, count;
        private Buffer buffer;

        private BulkReadInstruction(TokenActor a, BulkWritableConcreteStorage storage, int count) {
            assert a.isInput() : a;
            this.token = a.token();
            this.storage = storage;
            this.index = a.translateOutputIndex(0, 0);
            this.count = count;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
            if (!buffers.containsKey(token))
                return;
            if (buffer != null)
                checkState(buffers.get(token) == buffer, "reassigning %s from %s to %s", token, buffer,
                        buffers.get(token));
            this.buffer = buffers.get(token);
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, count);
        }

        @Override
        public boolean load() {
            if (buffer.size() < count)
                return false;
            storage.bulkWrite(buffer, index, count);
            storage.sync();
            return true;
        }

        @Override
        public Map<Token, Object[]> unload() {
            Object[] data = new Object[count];
            for (int i = index; i < count; ++i) {
                data[i - index] = storage.read(i);
            }
            return ImmutableMap.of(token, data);
        }
    }

    /**
     * TODO: consider using read/write handles instead of read(), write()?
     */
    private static final class TokenReadInstruction implements ReadInstruction {
        private final Token token;
        private final IndexFunction idxFxn;
        private final ConcreteStorage storage;
        private final int count;
        private Buffer buffer;

        private TokenReadInstruction(TokenActor a, ConcreteStorage storage, int count) {
            assert a.isInput() : a;
            this.token = a.token();
            this.storage = storage;
            this.idxFxn = Iterables.getOnlyElement(a.outputIndexFunctions());
            this.count = count;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
            if (!buffers.containsKey(token))
                return;
            if (buffer != null)
                checkState(buffers.get(token) == buffer, "reassigning %s from %s to %s", token, buffer,
                        buffers.get(token));
            this.buffer = buffers.get(token);
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, count);
        }

        @Override
        public boolean load() {
            Object[] data = new Object[count];
            if (!buffer.readAll(data))
                return false;
            for (int i = 0; i < data.length; ++i) {
                int idx;
                try {
                    idx = idxFxn.applyAsInt(i);
                } catch (Throwable ex) {
                    throw new AssertionError("Can't happen! Index functions should not throw", ex);
                }
                storage.write(idx, data[i]);
            }
            storage.sync();
            return true;
        }

        @Override
        public Map<Token, Object[]> unload() {
            Object[] data = new Object[count];
            for (int i = 0; i < data.length; ++i) {
                int idx;
                try {
                    idx = idxFxn.applyAsInt(i);
                } catch (Throwable ex) {
                    throw new AssertionError("Can't happen! Index functions should not throw", ex);
                }
                data[i] = storage.read(idx);
            }
            return ImmutableMap.of(token, data);
        }
    }

    /**
     * Doesn't read anything, but does respond to getMinimumBufferCapacity().
     */
    private static final class NopReadInstruction implements ReadInstruction {
        private final Token token;

        private NopReadInstruction(Token token) {
            this.token = token;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, 0);
        }

        @Override
        public boolean load() {
            return true;
        }

        @Override
        public Map<Token, Object[]> unload() {
            return ImmutableMap.of();
        }
    }

    private static final class BulkWriteInstruction implements WriteInstruction {
        private final Token token;
        private final BulkReadableConcreteStorage storage;
        private final int index, count;
        private Buffer buffer;
        private int written;

        private BulkWriteInstruction(TokenActor a, BulkReadableConcreteStorage storage, int count) {
            assert a.isOutput() : a;
            this.token = a.token();
            this.storage = storage;
            this.index = a.translateInputIndex(0, 0);
            this.count = count;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
            if (!buffers.containsKey(token))
                return;
            if (buffer != null)
                checkState(buffers.get(token) == buffer, "reassigning %s from %s to %s", token, buffer,
                        buffers.get(token));
            this.buffer = buffers.get(token);
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, count);
        }

        @Override
        public Boolean call() {
            written += storage.bulkRead(buffer, index + written, count);
            if (written < count)
                return false;
            written = 0;
            return true;
        }
    }

    /**
     * TODO: consider using read handles instead of read()?
     */
    private static final class TokenWriteInstruction implements WriteInstruction {
        private final Token token;
        private final IndexFunction idxFxn;
        private final ConcreteStorage storage;
        private final int count;
        private Buffer buffer;
        private int written;

        private TokenWriteInstruction(TokenActor a, ConcreteStorage storage, int count) {
            assert a.isOutput() : a;
            this.token = a.token();
            this.storage = storage;
            this.idxFxn = Iterables.getOnlyElement(a.inputIndexFunctions());
            this.count = count;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
            if (!buffers.containsKey(token))
                return;
            if (buffer != null)
                checkState(buffers.get(token) == buffer, "reassigning %s from %s to %s", token, buffer,
                        buffers.get(token));
            this.buffer = buffers.get(token);
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, count);
        }

        @Override
        public Boolean call() {
            Object[] data = new Object[count];
            for (int i = 0; i < count; ++i) {
                int idx;
                try {
                    idx = idxFxn.applyAsInt(i);
                } catch (Throwable ex) {
                    throw new AssertionError("Can't happen! Index functions should not throw", ex);
                }
                data[i] = storage.read(idx);
            }
            written += buffer.write(data, written, data.length - written);
            if (written < count)
                return false;
            written = 0;
            return true;
        }
    }

    /**
     * Doesn't write anything, but does respond to getMinimumBufferCapacity().
     */
    private static final class NopWriteInstruction implements WriteInstruction {
        private final Token token;

        private NopWriteInstruction(Token token) {
            this.token = token;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of(token, 0);
        }

        @Override
        public Boolean call() {
            return true;
        }
    }

    /**
     * Writes initial data into init storage, or "unloads" it (just returning it
     * as it was in the DrainData, not actually reading the storage) if we drain
     * during init.  (Any remaining data after init will be migrated as normal.)
     * There's only one of these per blob because it returns all the data, and
     * it should be the first initReadInstruction.
     */
    private static final class InitDataReadInstruction implements ReadInstruction {
        private final ImmutableMap<ConcreteStorage, ImmutableList<Pair<ImmutableList<Object>, IndexFunction>>> toWrite;
        private final ImmutableMap<Token, ImmutableList<Object>> initialStateDataMap;

        private InitDataReadInstruction(Map<Storage, ConcreteStorage> initStorage,
                ImmutableMap<Token, ImmutableList<Object>> initialStateDataMap) {
            ImmutableMap.Builder<ConcreteStorage, ImmutableList<Pair<ImmutableList<Object>, IndexFunction>>> toWriteBuilder = ImmutableMap
                    .builder();
            for (Map.Entry<Storage, ConcreteStorage> e : initStorage.entrySet()) {
                Storage s = e.getKey();
                if (s.isInternal())
                    continue;
                if (s.initialData().isEmpty())
                    continue;
                toWriteBuilder.put(e.getValue(), ImmutableList.copyOf(s.initialData()));
            }
            this.toWrite = toWriteBuilder.build();
            this.initialStateDataMap = initialStateDataMap;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of();
        }

        @Override
        public boolean load() {
            for (Map.Entry<ConcreteStorage, ImmutableList<Pair<ImmutableList<Object>, IndexFunction>>> e : toWrite
                    .entrySet())
                for (Pair<ImmutableList<Object>, IndexFunction> p : e.getValue())
                    for (int i = 0; i < p.first.size(); ++i) {
                        int idx;
                        try {
                            idx = p.second.applyAsInt(i);
                        } catch (Throwable ex) {
                            throw new AssertionError("Can't happen! Index functions should not throw", ex);
                        }
                        e.getKey().write(idx, p.first.get(i));
                    }
            return true;
        }

        @Override
        public Map<Token, Object[]> unload() {
            Map<Token, Object[]> r = new HashMap<>();
            for (Map.Entry<Token, ImmutableList<Object>> e : initialStateDataMap.entrySet())
                r.put(e.getKey(), e.getValue().toArray());
            return r;
        }
    }

    private static final class ReportThroughputInstruction implements ReadInstruction, WriteInstruction {
        private static final long WARMUP_NANOS = TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS);
        private static final long TIMING_NANOS = TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS);
        private final long throughputPerSteadyState;
        private int steadyStates = 0;
        private long firstNanoTime = Long.MIN_VALUE, afterWarmupNanoTime = Long.MIN_VALUE;

        private ReportThroughputInstruction(long throughputPerSteadyState) {
            this.throughputPerSteadyState = throughputPerSteadyState;
        }

        @Override
        public void init(Map<Token, Buffer> buffers) {
        }

        @Override
        public Map<Token, Integer> getMinimumBufferCapacity() {
            return ImmutableMap.of();
        }

        @Override
        public Map<Token, Object[]> unload() {
            return ImmutableMap.of();
        }

        @Override
        public boolean load() {
            long currentTime = time();
            if (firstNanoTime == Long.MIN_VALUE)
                firstNanoTime = currentTime;
            else if (afterWarmupNanoTime == Long.MIN_VALUE && currentTime - firstNanoTime > WARMUP_NANOS)
                afterWarmupNanoTime = currentTime;
            return true;
        }

        @Override
        public Boolean call() {
            if (afterWarmupNanoTime != Long.MIN_VALUE) {
                ++steadyStates;
                long currentTime = time();
                long elapsed = currentTime - afterWarmupNanoTime;
                if (elapsed > TIMING_NANOS) {
                    long itemsOutput = steadyStates * throughputPerSteadyState;
                    System.out.format("%d/%d/%d/%d#%n", steadyStates, itemsOutput, elapsed, elapsed / itemsOutput);
                    System.out.flush();
                    System.exit(0);
                }
            }
            return true;
        }

        private static long time() {
            //         return System.currentTimeMillis()*1000000;
            return System.nanoTime();
        }
    }

    /**
     * Creates the blob host.  This mostly involves shuffling our state into the
     * form the blob host wants.
     * @return the blob
     */
    public Blob instantiateBlob() {
        ImmutableSortedSet.Builder<Token> inputTokens = ImmutableSortedSet.naturalOrder(),
                outputTokens = ImmutableSortedSet.naturalOrder();
        for (TokenActor ta : Iterables.filter(actors, TokenActor.class))
            (ta.isInput() ? inputTokens : outputTokens).add(ta.token());
        ImmutableList.Builder<MethodHandle> storageAdjusts = ImmutableList.builder();
        for (ConcreteStorage s : steadyStateStorage.values())
            storageAdjusts.add(s.adjustHandle());
        return new Compiler2BlobHost(workers, config, inputTokens.build(), outputTokens.build(), initCode,
                steadyStateCode, storageAdjusts.build(), initReadInstructions, initWriteInstructions,
                migrationInstructions, readInstructions, writeInstructions, drainInstructions, precreatedBuffers);
    }

    private final Map<Integer, int[]> bulkCache = new HashMap<>();

    private int[] getBulk(int size) {
        int[] bulk = bulkCache.computeIfAbsent(size, int[]::new);
        for (int i = 0; i < bulk.length; ++i)
            bulk[i] = i;
        return bulk;
    }

    public static void main(String[] args) {
        StreamCompiler sc;
        Benchmark bm;
        if (args.length == 3) {
            String benchmarkName = args[0];
            int cores = Integer.parseInt(args[1]);
            int multiplier = Integer.parseInt(args[2]);
            sc = new Compiler2StreamCompiler().maxNumCores(cores).multiplier(multiplier);
            bm = Benchmarker.getBenchmarkByName(benchmarkName);
        } else {
            sc = new Compiler2StreamCompiler().multiplier(384).maxNumCores(4);
            bm = new FMRadio.FMRadioBenchmarkProvider().iterator().next();
        }
        Benchmarker.runBenchmark(bm, sc).get(0).print(System.out);
    }

    private void printDot(String stage) {
        try (FileWriter fw = new FileWriter(stage + ".dot"); BufferedWriter bw = new BufferedWriter(fw)) {
            bw.write("digraph {\n");
            for (ActorGroup g : groups) {
                bw.write("subgraph cluster_" + Integer.toString(g.id()).replace('-', '_') + " {\n");
                for (Actor a : g.actors())
                    bw.write(String.format("\"%s\";\n", nodeName(a)));
                bw.write("label = \"x" + externalSchedule.get(g) + "\";\n");
                bw.write("}\n");
            }
            for (ActorGroup g : groups) {
                for (Actor a : g.actors()) {
                    for (Storage s : a.outputs())
                        for (Actor b : s.downstream())
                            bw.write(String.format("\"%s\" -> \"%s\";\n", nodeName(a), nodeName(b)));
                }
            }
            bw.write("}\n");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private String nodeName(Actor a) {
        if (a instanceof TokenActor)
            return (((TokenActor) a).isInput()) ? "input" : "output";
        WorkerActor wa = (WorkerActor) a;
        String workerClassName = wa.worker().getClass().getSimpleName();
        return workerClassName.replaceAll("[a-z]", "") + "@" + Integer.toString(wa.id()).replace('-', '_');
    }
}