Index: lucene/analysis/kuromoji/src/tools/java/org/apache/lucene/analysis/ja/util/TokenInfoDictionaryBuilder.java
===================================================================
--- lucene/analysis/kuromoji/src/tools/java/org/apache/lucene/analysis/ja/util/TokenInfoDictionaryBuilder.java	(revision 1663077)
+++ lucene/analysis/kuromoji/src/tools/java/org/apache/lucene/analysis/ja/util/TokenInfoDictionaryBuilder.java	(working copy)
@@ -165,7 +165,7 @@
     
     final FST<Long> fst = fstBuilder.finish();
     
-    System.out.print("  " + fst.getNodeCount() + " nodes, " + fst.getArcCount() + " arcs, " + fst.ramBytesUsed() + " bytes...  ");
+    System.out.print("  " + fstBuilder.getNodeCount() + " nodes, " + fstBuilder.getArcCount() + " arcs, " + fst.ramBytesUsed() + " bytes...  ");
     dictionary.setFST(fst);
     System.out.println(" done");
     
Index: lucene/core/src/java/org/apache/lucene/codecs/blocktree/BlockTreeTermsReader.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/blocktree/BlockTreeTermsReader.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/blocktree/BlockTreeTermsReader.java	(working copy)
@@ -156,21 +156,19 @@
 
       for (int i = 0; i < numFields; ++i) {
         final int field = termsIn.readVInt();
+
+        final FieldInfo fieldInfo = state.fieldInfos.fieldInfo(field);
+        if (fieldInfo == null) {
+          throw new CorruptIndexException("invalid field number: " + field, termsIn);
+        }
+        if (fieldInfo.getIndexOptions() == IndexOptions.NONE) {
+          throw new CorruptIndexException("field \"" + fieldInfo.name + "\" was not indexed yet appears in the terms dictionary", termsIn);
+        }
         final long numTerms = termsIn.readVLong();
         if (numTerms <= 0) {
           throw new CorruptIndexException("Illegal numTerms for field number: " + field, termsIn);
         }
-        final int numBytes = termsIn.readVInt();
-        if (numBytes < 0) {
-          throw new CorruptIndexException("invalid rootCode for field number: " + field + ", numBytes=" + numBytes, termsIn);
-        }
-        final BytesRef rootCode = new BytesRef(new byte[numBytes]);
-        termsIn.readBytes(rootCode.bytes, 0, numBytes);
-        rootCode.length = numBytes;
-        final FieldInfo fieldInfo = state.fieldInfos.fieldInfo(field);
-        if (fieldInfo == null) {
-          throw new CorruptIndexException("invalid field number: " + field, termsIn);
-        }
+        final byte[] rootCode = readByteArray(fieldInfo.name, termsIn);
         final long sumTotalTermFreq = fieldInfo.getIndexOptions() == IndexOptions.DOCS ? -1 : termsIn.readVLong();
         final long sumDocFreq = termsIn.readVLong();
         final int docCount = termsIn.readVInt();
@@ -178,8 +176,8 @@
         if (longsSize < 0) {
           throw new CorruptIndexException("invalid longsSize for field: " + fieldInfo.name + ", longsSize=" + longsSize, termsIn);
         }
-        BytesRef minTerm = readBytesRef(termsIn);
-        BytesRef maxTerm = readBytesRef(termsIn);
+        byte[] minTerm = readByteArray(fieldInfo.name, termsIn);
+        byte[] maxTerm = readByteArray(fieldInfo.name, termsIn);
         if (docCount < 0 || docCount > state.segmentInfo.getDocCount()) { // #docs with field must be <= #docs
           throw new CorruptIndexException("invalid docCount: " + docCount + " maxDoc: " + state.segmentInfo.getDocCount(), termsIn);
         }
@@ -208,11 +206,13 @@
     }
   }
 
-  private static BytesRef readBytesRef(IndexInput in) throws IOException {
-    BytesRef bytes = new BytesRef();
-    bytes.length = in.readVInt();
-    bytes.bytes = new byte[bytes.length];
-    in.readBytes(bytes.bytes, 0, bytes.length);
+  private byte[] readByteArray(String field, IndexInput in) throws IOException {
+    int length = in.readVInt();
+    if (length < 0) {
+      throw new CorruptIndexException("invalid byte array length=" + length + " for field \"" + field, in);
+    }
+    byte[] bytes = new byte[length];
+    in.readBytes(bytes, 0, bytes.length);
     return bytes;
   }
 
Index: lucene/core/src/java/org/apache/lucene/codecs/blocktree/FieldReader.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/blocktree/FieldReader.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/blocktree/FieldReader.java	(working copy)
@@ -18,6 +18,7 @@
  */
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -42,8 +43,7 @@
 public final class FieldReader extends Terms implements Accountable {
 
   private static final long BASE_RAM_BYTES_USED =
-      RamUsageEstimator.shallowSizeOfInstance(FieldReader.class)
-      + 3 * RamUsageEstimator.shallowSizeOfInstance(BytesRef.class);
+    RamUsageEstimator.shallowSizeOfInstance(FieldReader.class);
 
   final long numTerms;
   final FieldInfo fieldInfo;
@@ -50,11 +50,9 @@
   final long sumTotalTermFreq;
   final long sumDocFreq;
   final int docCount;
-  final long indexStartFP;
-  final long rootBlockFP;
-  final BytesRef rootCode;
-  final BytesRef minTerm;
-  final BytesRef maxTerm;
+  final byte[] rootCode;
+  final byte[] minTerm;
+  final byte[] maxTerm;
   final int longsSize;
   final BlockTreeTermsReader parent;
 
@@ -61,9 +59,8 @@
   final FST<BytesRef> index;
   //private boolean DEBUG;
 
-  FieldReader(BlockTreeTermsReader parent, FieldInfo fieldInfo, long numTerms, BytesRef rootCode, long sumTotalTermFreq, long sumDocFreq, int docCount,
-              long indexStartFP, int longsSize, IndexInput indexIn, BytesRef minTerm, BytesRef maxTerm) throws IOException {
-    assert numTerms > 0;
+  FieldReader(BlockTreeTermsReader parent, FieldInfo fieldInfo, long numTerms, byte[] rootCode, long sumTotalTermFreq, long sumDocFreq, int docCount,
+              long indexStartFP, int longsSize, IndexInput indexIn, byte[] minTerm, byte[] maxTerm) throws IOException {
     this.fieldInfo = fieldInfo;
     //DEBUG = BlockTreeTermsReader.DEBUG && fieldInfo.name.equals("id");
     this.parent = parent;
@@ -71,17 +68,18 @@
     this.sumTotalTermFreq = sumTotalTermFreq; 
     this.sumDocFreq = sumDocFreq; 
     this.docCount = docCount;
-    this.indexStartFP = indexStartFP;
     this.rootCode = rootCode;
     this.longsSize = longsSize;
     this.minTerm = minTerm;
-    this.maxTerm = maxTerm;
+    if (minTerm != null && Arrays.equals(minTerm, maxTerm)) {
+      this.maxTerm = minTerm;
+    } else {
+      this.maxTerm = maxTerm;
+    }
     // if (DEBUG) {
     //   System.out.println("BTTR: seg=" + segment + " field=" + fieldInfo.name + " rootBlockCode=" + rootCode + " divisor=" + indexDivisor);
     // }
 
-    rootBlockFP = (new ByteArrayDataInput(rootCode.bytes, rootCode.offset, rootCode.length)).readVLong() >>> BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
-
     if (indexIn != null) {
       final IndexInput clone = indexIn.clone();
       //System.out.println("start=" + indexStartFP + " field=" + fieldInfo.name);
@@ -108,7 +106,7 @@
       // Older index that didn't store min/maxTerm
       return super.getMin();
     } else {
-      return minTerm;
+      return new BytesRef(minTerm);
     }
   }
 
@@ -118,7 +116,7 @@
       // Older index that didn't store min/maxTerm
       return super.getMax();
     } else {
-      return maxTerm;
+      return new BytesRef(maxTerm);
     }
   }
 
@@ -182,7 +180,7 @@
   }
     
   @Override
-  public long ramBytesUsed() {
+  public long ramBytesUsed() { 
     return BASE_RAM_BYTES_USED + ((index!=null)? index.ramBytesUsed() : 0);
   }
 
Index: lucene/core/src/java/org/apache/lucene/codecs/blocktree/IntersectTermsEnum.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/blocktree/IntersectTermsEnum.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/blocktree/IntersectTermsEnum.java	(working copy)
@@ -22,6 +22,7 @@
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.TermState;
 import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.ByteArrayDataInput;
 import org.apache.lucene.store.IndexInput;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.Bits;
@@ -93,12 +94,12 @@
 
     // Special pushFrame since it's the first one:
     final IntersectTermsEnumFrame f = stack[0];
-    f.fp = f.fpOrig = fr.rootBlockFP;
+    f.fp = f.fpOrig = (new ByteArrayDataInput(fr.rootCode)).readVLong() >>> BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
     f.prefix = 0;
     f.setState(runAutomaton.getInitialState());
     f.arc = arc;
     f.outputPrefix = arc.output;
-    f.load(fr.rootCode);
+    f.load(new BytesRef(fr.rootCode));
 
     // for assert:
     assert setSavedStartTerm(startTerm);
Index: lucene/core/src/java/org/apache/lucene/codecs/blocktree/SegmentTermsEnum.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/blocktree/SegmentTermsEnum.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/blocktree/SegmentTermsEnum.java	(working copy)
@@ -121,8 +121,6 @@
 
     Stats stats = new Stats(fr.parent.segment, fr.fieldInfo.name);
     if (fr.index != null) {
-      stats.indexNodeCount = fr.index.getNodeCount();
-      stats.indexArcCount = fr.index.getArcCount();
       stats.indexNumBytes = fr.index.ramBytesUsed();
     }
         
@@ -138,7 +136,7 @@
 
     // Empty string prefix must have an output in the
     // index!
-    currentFrame = pushFrame(arc, fr.rootCode, 0);
+    currentFrame = pushFrame(arc, new BytesRef(fr.rootCode), 0);
     currentFrame.fpOrig = currentFrame.fp;
     currentFrame.loadBlock();
     validIndexPrefix = 0;
@@ -197,7 +195,7 @@
     } else {
       arc = null;
     }
-    currentFrame = pushFrame(arc, fr.rootCode, 0);
+    currentFrame = pushFrame(arc, new BytesRef(fr.rootCode), 0);
     currentFrame.rewind();
     currentFrame.loadBlock();
     validIndexPrefix = 0;
@@ -879,7 +877,7 @@
       } else {
         arc = null;
       }
-      currentFrame = pushFrame(arc, fr.rootCode, 0);
+      currentFrame = pushFrame(arc, new BytesRef(fr.rootCode), 0);
       currentFrame.loadBlock();
     }
 
Index: lucene/core/src/java/org/apache/lucene/codecs/blocktree/Stats.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/blocktree/Stats.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/blocktree/Stats.java	(working copy)
@@ -33,12 +33,6 @@
  * @lucene.internal
  */
 public class Stats {
-  /** How many nodes in the index FST. */
-  public long indexNodeCount;
-
-  /** How many arcs in the index FST. */
-  public long indexArcCount;
-
   /** Byte size of the index. */
   public long indexNumBytes;
 
@@ -161,8 +155,6 @@
     }
       
     out.println("  index FST:");
-    out.println("    " + indexNodeCount + " nodes");
-    out.println("    " + indexArcCount + " arcs");
     out.println("    " + indexNumBytes + " bytes");
     out.println("  terms:");
     out.println("    " + totalTermCount + " terms");
Index: lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50FieldInfosFormat.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50FieldInfosFormat.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50FieldInfosFormat.java	(working copy)
@@ -19,6 +19,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.lucene.codecs.CodecUtil;
@@ -119,6 +120,8 @@
         
         final int size = input.readVInt(); //read in the size
         infos = new FieldInfo[size];
+
+        Map<String,String> lastAttributes = null;
         
         for (int i = 0; i < size; i++) {
           String name = input.readString();
@@ -136,10 +139,22 @@
           // DV Types are packed in one byte
           final DocValuesType docValuesType = getDocValuesType(input, input.readByte());
           final long dvGen = input.readLong();
-          final Map<String,String> attributes = input.readStringStringMap();
+          final Map<String,String> attributesIn = input.readStringStringMap();
+
+          final Map<String,String> attributes;
+          if (attributesIn.isEmpty()) {
+            attributes = attributesIn;
+          } else if (attributesIn.equals(lastAttributes)) {
+            // Use a single map in the common case when all fields that have any attributes set, have the same attributes:
+            attributes = lastAttributes;
+          } else {
+            attributes = Collections.unmodifiableMap(attributesIn);
+            lastAttributes = attributes;
+          }
+
           try {
             infos[i] = new FieldInfo(name, fieldNumber, storeTermVector, omitNorms, storePayloads, 
-                                     indexOptions, docValuesType, dvGen, Collections.unmodifiableMap(attributes));
+                                     indexOptions, docValuesType, dvGen, attributes);
             infos[i].checkConsistency();
           } catch (IllegalStateException e) {
             throw new CorruptIndexException("invalid fieldinfo for field: " + name + ", fieldNumber=" + fieldNumber, input, e);
Index: lucene/core/src/java/org/apache/lucene/store/DataInput.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/store/DataInput.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/store/DataInput.java	(working copy)
@@ -19,6 +19,7 @@
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -256,8 +257,12 @@
   /** Reads a Map&lt;String,String&gt; previously written
    *  with {@link DataOutput#writeStringStringMap(Map)}. */
   public Map<String,String> readStringStringMap() throws IOException {
+    final int count = readInt();
+    if (count == 0) {
+      return Collections.<String,String>emptyMap();
+    }
+
     final Map<String,String> map = new HashMap<>();
-    final int count = readInt();
     for(int i=0;i<count;i++) {
       final String key = readString();
       final String val = readString();
Index: lucene/core/src/java/org/apache/lucene/util/fst/Builder.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/fst/Builder.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/util/fst/Builder.java	(working copy)
@@ -52,7 +52,7 @@
 
 public class Builder<T> {
   private final NodeHash<T> dedupHash;
-  private final FST<T> fst;
+  final FST<T> fst;
   private final T NO_OUTPUT;
 
   // private static final boolean DEBUG = true;
@@ -81,6 +81,22 @@
   // current "frontier"
   private UnCompiledNode<T>[] frontier;
 
+  // Used for the BIT_TARGET_NEXT optimization (whereby
+  // instead of storing the address of the target node for
+  // a given arc, we mark a single bit noting that the next
+  // node in the byte[] is the target node):
+  long lastFrozenNode;
+
+  // Reused temporarily while building the FST:
+  int[] reusedBytesPerArc = new int[4];
+
+  long arcCount;
+  long nodeCount;
+
+  boolean allowArrayArcs;
+
+  BytesStore bytes;
+
   /**
    * Instantiates an FST/FSA builder without any pruning. A shortcut
    * to {@link #Builder(FST.INPUT_TYPE, int, int, boolean,
@@ -152,9 +168,12 @@
     this.shareMaxTailLength = shareMaxTailLength;
     this.doPackFST = doPackFST;
     this.acceptableOverheadRatio = acceptableOverheadRatio;
-    fst = new FST<>(inputType, outputs, doPackFST, acceptableOverheadRatio, allowArrayArcs, bytesPageBits);
+    this.allowArrayArcs = allowArrayArcs;
+    fst = new FST<>(inputType, outputs, doPackFST, acceptableOverheadRatio, bytesPageBits);
+    bytes = fst.bytes;
+    assert bytes != null;
     if (doShareSuffix) {
-      dedupHash = new NodeHash<>(fst, fst.bytes.getReverseReader(false));
+      dedupHash = new NodeHash<>(fst, bytes.getReverseReader(false));
     } else {
       dedupHash = null;
     }
@@ -168,31 +187,45 @@
     }
   }
 
-  public long getTotStateCount() {
-    return fst.nodeCount;
-  }
-
   public long getTermCount() {
     return frontier[0].inputCount;
   }
 
+  public long getNodeCount() {
+    // 1+ in order to count the -1 implicit final node
+    return 1+nodeCount;
+  }
+  
+  public long getArcCount() {
+    return arcCount;
+  }
+
   public long getMappedStateCount() {
-    return dedupHash == null ? 0 : fst.nodeCount;
+    return dedupHash == null ? 0 : nodeCount;
   }
 
   private CompiledNode compileNode(UnCompiledNode<T> nodeIn, int tailLength) throws IOException {
     final long node;
+    long bytesPosStart = bytes.getPosition();
     if (dedupHash != null && (doShareNonSingletonNodes || nodeIn.numArcs <= 1) && tailLength <= shareMaxTailLength) {
       if (nodeIn.numArcs == 0) {
-        node = fst.addNode(nodeIn);
+        node = fst.addNode(this, nodeIn);
+        lastFrozenNode = node;
       } else {
-        node = dedupHash.add(nodeIn);
+        node = dedupHash.add(this, nodeIn);
       }
     } else {
-      node = fst.addNode(nodeIn);
+      node = fst.addNode(this, nodeIn);
     }
     assert node != -2;
 
+    long bytesPosEnd = bytes.getPosition();
+    if (bytesPosEnd != bytesPosStart) {
+      // The FST added a new node:
+      assert bytesPosEnd > bytesPosStart;
+      lastFrozenNode = node;
+    }
+
     nodeIn.clear();
 
     final CompiledNode fn = new CompiledNode();
@@ -464,7 +497,7 @@
     fst.finish(compileNode(root, lastInput.length()).node);
 
     if (doPackFST) {
-      return fst.pack(3, Math.max(10, (int) (fst.getNodeCount()/4)), acceptableOverheadRatio);
+      return fst.pack(this, 3, Math.max(10, (int) (getNodeCount()/4)), acceptableOverheadRatio);
     } else {
       return fst;
     }
Index: lucene/core/src/java/org/apache/lucene/util/fst/FST.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/fst/FST.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/util/fst/FST.java	(working copy)
@@ -78,19 +78,18 @@
   /** Specifies allowed range of each int input label for
    *  this FST. */
   public static enum INPUT_TYPE {BYTE1, BYTE2, BYTE4};
-  public final INPUT_TYPE inputType;
 
-  final static int BIT_FINAL_ARC = 1 << 0;
-  final static int BIT_LAST_ARC = 1 << 1;
-  final static int BIT_TARGET_NEXT = 1 << 2;
+  static final int BIT_FINAL_ARC = 1 << 0;
+  static final int BIT_LAST_ARC = 1 << 1;
+  static final int BIT_TARGET_NEXT = 1 << 2;
 
   // TODO: we can free up a bit if we can nuke this:
-  final static int BIT_STOP_NODE = 1 << 3;
+  static final int BIT_STOP_NODE = 1 << 3;
 
   /** This flag is set if the arc has an output. */
-  public final static int BIT_ARC_HAS_OUTPUT = 1 << 4;
+  public static final int BIT_ARC_HAS_OUTPUT = 1 << 4;
 
-  final static int BIT_ARC_HAS_FINAL_OUTPUT = 1 << 5;
+  static final int BIT_ARC_HAS_FINAL_OUTPUT = 1 << 5;
 
   // Arcs are stored as fixed-size (per entry) array, so
   // that we can find an arc using binary search.  We do
@@ -98,91 +97,85 @@
 
   // If set, the target node is delta coded vs current
   // position:
-  private final static int BIT_TARGET_DELTA = 1 << 6;
+  private static final int BIT_TARGET_DELTA = 1 << 6;
 
   // We use this as a marker (because this one flag is
   // illegal by itself ...):
-  private final static byte ARCS_AS_FIXED_ARRAY = BIT_ARC_HAS_FINAL_OUTPUT;
+  private static final byte ARCS_AS_FIXED_ARRAY = BIT_ARC_HAS_FINAL_OUTPUT;
 
   /**
    * @see #shouldExpand(UnCompiledNode)
    */
-  final static int FIXED_ARRAY_SHALLOW_DISTANCE = 3; // 0 => only root node.
+  static final int FIXED_ARRAY_SHALLOW_DISTANCE = 3; // 0 => only root node.
 
   /**
    * @see #shouldExpand(UnCompiledNode)
    */
-  final static int FIXED_ARRAY_NUM_ARCS_SHALLOW = 5;
+  static final int FIXED_ARRAY_NUM_ARCS_SHALLOW = 5;
 
   /**
    * @see #shouldExpand(UnCompiledNode)
    */
-  final static int FIXED_ARRAY_NUM_ARCS_DEEP = 10;
+  static final int FIXED_ARRAY_NUM_ARCS_DEEP = 10;
 
-  // Reused temporarily while building the FST:
-  private int[] reusedBytesPerArc = new int[0];
-
   // Increment version to change it
-  private final static String FILE_FORMAT_NAME = "FST";
-  private final static int VERSION_START = 0;
+  private static final String FILE_FORMAT_NAME = "FST";
+  private static final int VERSION_START = 0;
 
   /** Changed numBytesPerArc for array'd case from byte to int. */
-  private final static int VERSION_INT_NUM_BYTES_PER_ARC = 1;
+  private static final int VERSION_INT_NUM_BYTES_PER_ARC = 1;
 
   /** Write BYTE2 labels as 2-byte short, not vInt. */
-  private final static int VERSION_SHORT_BYTE2_LABELS = 2;
+  private static final int VERSION_SHORT_BYTE2_LABELS = 2;
 
   /** Added optional packed format. */
-  private final static int VERSION_PACKED = 3;
+  private static final int VERSION_PACKED = 3;
 
   /** Changed from int to vInt for encoding arc targets. 
    *  Also changed maxBytesPerArc from int to vInt in the array case. */
-  private final static int VERSION_VINT_TARGET = 4;
+  private static final int VERSION_VINT_TARGET = 4;
 
-  private final static int VERSION_CURRENT = VERSION_VINT_TARGET;
+  /** Don't store arcWithOutputCount anymore */
+  private static final int VERSION_NO_NODE_ARC_COUNTS = 5;
 
+  private static final int VERSION_CURRENT = VERSION_NO_NODE_ARC_COUNTS;
+
   // Never serialized; just used to represent the virtual
   // final node w/ no arcs:
-  private final static long FINAL_END_NODE = -1;
+  private static final long FINAL_END_NODE = -1;
 
   // Never serialized; just used to represent the virtual
   // non-final node w/ no arcs:
-  private final static long NON_FINAL_END_NODE = 0;
+  private static final long NON_FINAL_END_NODE = 0;
 
+  /** If arc has this label then that arc is final/accepted */
+  public static final int END_LABEL = -1;
+
+  public final INPUT_TYPE inputType;
+
   // if non-null, this FST accepts the empty string and
   // produces this output
   T emptyOutput;
 
+  /** A {@link BytesStore}, used during building, or during reading when
+   *  the FST is very large (more than 1 GB).  If the FST is less than 1
+   *  GB then bytesArray is set instead. */
   final BytesStore bytes;
 
+  /** Used at read time when the FST fits into a single byte[]. */
+  final byte[] bytesArray;
+
   private long startNode = -1;
 
   public final Outputs<T> outputs;
 
-  // Used for the BIT_TARGET_NEXT optimization (whereby
-  // instead of storing the address of the target node for
-  // a given arc, we mark a single bit noting that the next
-  // node in the byte[] is the target node):
-  private long lastFrozenNode;
-
-  private final T NO_OUTPUT;
-
-  public long nodeCount;
-  public long arcCount;
-  public long arcWithOutputCount;
-
   private final boolean packed;
   private PackedInts.Reader nodeRefToAddress;
 
-  /** If arc has this label then that arc is final/accepted */
-  public static final int END_LABEL = -1;
-
-  private final boolean allowArrayArcs;
-
   private Arc<T> cachedRootArcs[];
 
   /** Represents a single arc. */
-  public final static class Arc<T> {
+  public static final class Arc<T> {
     public int label;
     public T output;
 
@@ -292,16 +285,15 @@
 
   // make a new empty FST, for building; Builder invokes
   // this ctor
-  FST(INPUT_TYPE inputType, Outputs<T> outputs, boolean willPackFST, float acceptableOverheadRatio, boolean allowArrayArcs, int bytesPageBits) {
+  FST(INPUT_TYPE inputType, Outputs<T> outputs, boolean willPackFST, float acceptableOverheadRatio, int bytesPageBits) {
     this.inputType = inputType;
     this.outputs = outputs;
-    this.allowArrayArcs = allowArrayArcs;
     version = VERSION_CURRENT;
+    bytesArray = null;
     bytes = new BytesStore(bytesPageBits);
     // pad: ensure no node gets address 0 which is reserved to mean
     // the stop state w/ no arcs
     bytes.writeByte((byte) 0);
-    NO_OUTPUT = outputs.getNoOutput();
     if (willPackFST) {
       nodeAddress = new GrowableWriter(15, 8, acceptableOverheadRatio);
       inCounts = new GrowableWriter(1, 8, acceptableOverheadRatio);
@@ -333,7 +325,7 @@
 
     // NOTE: only reads most recent format; we don't have
     // back-compat promise for FSTs (they are experimental):
-    version = CodecUtil.checkHeader(in, FILE_FORMAT_NAME, VERSION_PACKED, VERSION_VINT_TARGET);
+    version = CodecUtil.checkHeader(in, FILE_FORMAT_NAME, VERSION_PACKED, VERSION_NO_NODE_ARC_COUNTS);
     packed = in.readByte() == 1;
     if (in.readByte() == 1) {
       // accepts empty string
@@ -379,30 +371,25 @@
       nodeRefToAddress = null;
     }
     startNode = in.readVLong();
-    nodeCount = in.readVLong();
-    arcCount = in.readVLong();
-    arcWithOutputCount = in.readVLong();
+    if (version < VERSION_NO_NODE_ARC_COUNTS) {
+      in.readVLong();
+      in.readVLong();
+      in.readVLong();
+    }
 
     long numBytes = in.readVLong();
-    bytes = new BytesStore(in, numBytes, 1<<maxBlockBits);
+    if (numBytes > 1 << maxBlockBits) {
+      // FST is big: we need multiple pages
+      bytes = new BytesStore(in, numBytes, 1<<maxBlockBits);
+      bytesArray = null;
+    } else {
+      // FST fits into a single block: use ByteArrayBytesStoreReader for less overhead
+      bytes = null;
+      bytesArray = new byte[(int) numBytes];
+      in.readBytes(bytesArray, 0, bytesArray.length);
+    }
     
-    NO_OUTPUT = outputs.getNoOutput();
-
     cacheRootArcs();
-
-    // NOTE: bogus because this is only used during
-    // building; we need to break out mutable FST from
-    // immutable
-    allowArrayArcs = false;
-
-    /*
-    if (bytes.length == 665) {
-      Writer w = new OutputStreamWriter(new FileOutputStream("out.dot"), StandardCharsets.UTF_8);
-      Util.toDot(this, w, false, false);
-      w.close();
-      System.out.println("Wrote FST to out.dot");
-    }
-    */
   }
 
   public INPUT_TYPE getInputType() {
@@ -433,7 +420,11 @@
   @Override
   public long ramBytesUsed() {
     long size = BASE_RAM_BYTES_USED;
-    size += bytes.ramBytesUsed();
+    if (bytesArray != null) {
+      size += bytesArray.length;
+    } else {
+      size += bytes.ramBytesUsed();
+    }
     if (packed) {
       size += nodeRefToAddress.ramBytesUsed();
     } else if (nodeAddress != null) {
@@ -441,7 +432,6 @@
       size += inCounts.ramBytesUsed();
     }
     size += cachedArcsBytesUsed;
-    size += RamUsageEstimator.sizeOf(reusedBytesPerArc);
     return size;
   }
 
@@ -459,10 +449,11 @@
 
   @Override
   public String toString() {
-    return getClass().getSimpleName() + "(input=" + inputType + ",output=" + outputs + ",packed=" + packed + ",nodes=" + nodeCount + ",arcs=" + arcCount + ")";
+    return getClass().getSimpleName() + "(input=" + inputType + ",output=" + outputs + ",packed=" + packed;
   }
 
   void finish(long newStartNode) throws IOException {
+    assert newStartNode <= bytes.getPosition();
     if (startNode != -1) {
       throw new IllegalStateException("already finished");
     }
@@ -471,7 +462,6 @@
     }
     startNode = newStartNode;
     bytes.finish();
-
     cacheRootArcs();
   }
 
@@ -592,12 +582,15 @@
       ((PackedInts.Mutable) nodeRefToAddress).save(out);
     }
     out.writeVLong(startNode);
-    out.writeVLong(nodeCount);
-    out.writeVLong(arcCount);
-    out.writeVLong(arcWithOutputCount);
-    long numBytes = bytes.getPosition();
-    out.writeVLong(numBytes);
-    bytes.writeTo(out);
+    if (bytes != null) {
+      long numBytes = bytes.getPosition();
+      out.writeVLong(numBytes);
+      bytes.writeTo(out);
+    } else {
+      assert bytesArray != null;
+      out.writeVLong(bytesArray.length);
+      out.writeBytes(bytesArray, 0, bytesArray.length);
+    }
   }
   
   /**
@@ -654,7 +647,8 @@
 
   // serializes new node by appending its bytes to the end
   // of the current byte[]
-  long addNode(Builder.UnCompiledNode<T> nodeIn) throws IOException {
+  long addNode(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn) throws IOException {
+    T NO_OUTPUT = outputs.getNoOutput();
 
     //System.out.println("FST.addNode pos=" + bytes.getPosition() + " numArcs=" + nodeIn.numArcs);
     if (nodeIn.numArcs == 0) {
@@ -665,22 +659,22 @@
       }
     }
 
-    final long startAddress = bytes.getPosition();
+    final long startAddress = builder.bytes.getPosition();
     //System.out.println("  startAddr=" + startAddress);
 
-    final boolean doFixedArray = shouldExpand(nodeIn);
+    final boolean doFixedArray = shouldExpand(builder, nodeIn);
     if (doFixedArray) {
       //System.out.println("  fixedArray");
-      if (reusedBytesPerArc.length < nodeIn.numArcs) {
-        reusedBytesPerArc = new int[ArrayUtil.oversize(nodeIn.numArcs, 1)];
+      if (builder.reusedBytesPerArc.length < nodeIn.numArcs) {
+        builder.reusedBytesPerArc = new int[ArrayUtil.oversize(nodeIn.numArcs, 1)];
       }
     }
 
-    arcCount += nodeIn.numArcs;
+    builder.arcCount += nodeIn.numArcs;
     
     final int lastArc = nodeIn.numArcs-1;
 
-    long lastArcStart = bytes.getPosition();
+    long lastArcStart = builder.bytes.getPosition();
     int maxBytesPerArc = 0;
     for(int arcIdx=0;arcIdx<nodeIn.numArcs;arcIdx++) {
       final Builder.Arc<T> arc = nodeIn.arcs[arcIdx];
@@ -692,7 +686,7 @@
         flags += BIT_LAST_ARC;
       }
 
-      if (lastFrozenNode == target.node && !doFixedArray) {
+      if (builder.lastFrozenNode == target.node && !doFixedArray) {
         // TODO: for better perf (but more RAM used) we
         // could avoid this except when arc is "near" the
         // last arc:
@@ -720,26 +714,25 @@
         flags += BIT_ARC_HAS_OUTPUT;
       }
 
-      bytes.writeByte((byte) flags);
-      writeLabel(bytes, arc.label);
+      builder.bytes.writeByte((byte) flags);
+      writeLabel(builder.bytes, arc.label);
 
       // System.out.println("  write arc: label=" + (char) arc.label + " flags=" + flags + " target=" + target.node + " pos=" + bytes.getPosition() + " output=" + outputs.outputToString(arc.output));
 
       if (arc.output != NO_OUTPUT) {
-        outputs.write(arc.output, bytes);
+        outputs.write(arc.output, builder.bytes);
         //System.out.println("    write output");
-        arcWithOutputCount++;
       }
 
       if (arc.nextFinalOutput != NO_OUTPUT) {
         //System.out.println("    write final output");
-        outputs.writeFinalOutput(arc.nextFinalOutput, bytes);
+        outputs.writeFinalOutput(arc.nextFinalOutput, builder.bytes);
       }
 
       if (targetHasArcs && (flags & BIT_TARGET_NEXT) == 0) {
         assert target.node > 0;
         //System.out.println("    write target");
-        bytes.writeVLong(target.node);
+        builder.bytes.writeVLong(target.node);
       }
 
       // just write the arcs "like normal" on first pass,
@@ -746,10 +739,10 @@
       // but record how many bytes each one took, and max
       // byte size:
       if (doFixedArray) {
-        reusedBytesPerArc[arcIdx] = (int) (bytes.getPosition() - lastArcStart);
-        lastArcStart = bytes.getPosition();
-        maxBytesPerArc = Math.max(maxBytesPerArc, reusedBytesPerArc[arcIdx]);
-        //System.out.println("    bytes=" + reusedBytesPerArc[arcIdx]);
+        builder.reusedBytesPerArc[arcIdx] = (int) (builder.bytes.getPosition() - lastArcStart);
+        lastArcStart = builder.bytes.getPosition();
+        maxBytesPerArc = Math.max(maxBytesPerArc, builder.reusedBytesPerArc[arcIdx]);
+        //System.out.println("    bytes=" + builder.reusedBytesPerArc[arcIdx]);
       }
     }
     
@@ -793,53 +786,52 @@
       final long fixedArrayStart = startAddress + headerLen;
 
       // expand the arcs in place, backwards
-      long srcPos = bytes.getPosition();
+      long srcPos = builder.bytes.getPosition();
       long destPos = fixedArrayStart + nodeIn.numArcs*maxBytesPerArc;
       assert destPos >= srcPos;
       if (destPos > srcPos) {
-        bytes.skipBytes((int) (destPos - srcPos));
+        builder.bytes.skipBytes((int) (destPos - srcPos));
         for(int arcIdx=nodeIn.numArcs-1;arcIdx>=0;arcIdx--) {
           destPos -= maxBytesPerArc;
-          srcPos -= reusedBytesPerArc[arcIdx];
+          srcPos -= builder.reusedBytesPerArc[arcIdx];
           //System.out.println("  repack arcIdx=" + arcIdx + " srcPos=" + srcPos + " destPos=" + destPos);
           if (srcPos != destPos) {
-            //System.out.println("  copy len=" + reusedBytesPerArc[arcIdx]);
-            assert destPos > srcPos: "destPos=" + destPos + " srcPos=" + srcPos + " arcIdx=" + arcIdx + " maxBytesPerArc=" + maxBytesPerArc + " reusedBytesPerArc[arcIdx]=" + reusedBytesPerArc[arcIdx] + " nodeIn.numArcs=" + nodeIn.numArcs;
-            bytes.copyBytes(srcPos, destPos, reusedBytesPerArc[arcIdx]);
+            //System.out.println("  copy len=" + builder.reusedBytesPerArc[arcIdx]);
+            assert destPos > srcPos: "destPos=" + destPos + " srcPos=" + srcPos + " arcIdx=" + arcIdx + " maxBytesPerArc=" + maxBytesPerArc + " reusedBytesPerArc[arcIdx]=" + builder.reusedBytesPerArc[arcIdx] + " nodeIn.numArcs=" + nodeIn.numArcs;
+            builder.bytes.copyBytes(srcPos, destPos, builder.reusedBytesPerArc[arcIdx]);
           }
         }
       }
       
       // now write the header
-      bytes.writeBytes(startAddress, header, 0, headerLen);
+      builder.bytes.writeBytes(startAddress, header, 0, headerLen);
     }
 
-    final long thisNodeAddress = bytes.getPosition()-1;
+    final long thisNodeAddress = builder.bytes.getPosition()-1;
 
-    bytes.reverse(startAddress, thisNodeAddress);
+    builder.bytes.reverse(startAddress, thisNodeAddress);
 
     // PackedInts uses int as the index, so we cannot handle
     // > 2.1B nodes when packing:
-    if (nodeAddress != null && nodeCount == Integer.MAX_VALUE) {
+    if (nodeAddress != null && builder.nodeCount == Integer.MAX_VALUE) {
       throw new IllegalStateException("cannot create a packed FST with more than 2.1 billion nodes");
     }
 
-    nodeCount++;
+    builder.nodeCount++;
     final long node;
     if (nodeAddress != null) {
 
       // Nodes are addressed by 1+ord:
-      if ((int) nodeCount == nodeAddress.size()) {
+      if ((int) builder.nodeCount == nodeAddress.size()) {
         nodeAddress = nodeAddress.resize(ArrayUtil.oversize(nodeAddress.size() + 1, nodeAddress.getBitsPerValue()));
         inCounts = inCounts.resize(ArrayUtil.oversize(inCounts.size() + 1, inCounts.getBitsPerValue()));
       }
-      nodeAddress.set((int) nodeCount, thisNodeAddress);
+      nodeAddress.set((int) builder.nodeCount, thisNodeAddress);
       // System.out.println("  write nodeAddress[" + nodeCount + "] = " + endAddress);
-      node = nodeCount;
+      node = builder.nodeCount;
     } else {
       node = thisNodeAddress;
     }
-    lastFrozenNode = node;
 
     //System.out.println("  ret node=" + node + " address=" + thisNodeAddress + " nodeAddress=" + nodeAddress);
     return node;
@@ -848,6 +840,7 @@
   /** Fills virtual 'start' arc, ie, an empty incoming arc to
    *  the FST's start node */
   public Arc<T> getFirstArc(Arc<T> arc) {
+    T NO_OUTPUT = outputs.getNoOutput();
 
     if (emptyOutput != null) {
       arc.flags = BIT_FINAL_ARC | BIT_LAST_ARC;
@@ -1324,19 +1317,6 @@
     }
   }
 
-  public long getNodeCount() {
-    // 1+ in order to count the -1 implicit final node
-    return 1+nodeCount;
-  }
-  
-  public long getArcCount() {
-    return arcCount;
-  }
-
-  public long getArcWithOutputCount() {
-    return arcWithOutputCount;
-  }
-
   /**
    * Nodes will be expanded if their depth (distance from the root node) is
    * &lt;= this value and their number of arcs is &gt;=
@@ -1352,8 +1332,8 @@
    * @see #FIXED_ARRAY_NUM_ARCS_DEEP
    * @see Builder.UnCompiledNode#depth
    */
-  private boolean shouldExpand(UnCompiledNode<T> node) {
-    return allowArrayArcs &&
+  private boolean shouldExpand(Builder<T> builder, UnCompiledNode<T> node) {
+    return builder.allowArrayArcs &&
       ((node.depth <= FIXED_ARRAY_SHALLOW_DISTANCE && node.numArcs >= FIXED_ARRAY_NUM_ARCS_SHALLOW) || 
        node.numArcs >= FIXED_ARRAY_NUM_ARCS_DEEP);
   }
@@ -1361,13 +1341,19 @@
   /** Returns a {@link BytesReader} for this FST, positioned at
    *  position 0. */
   public BytesReader getBytesReader() {
-    BytesReader in;
     if (packed) {
-      in = bytes.getForwardReader();
+      if (bytesArray != null) {
+        return new ForwardBytesReader(bytesArray);
+      } else {
+        return bytes.getForwardReader();
+      }
     } else {
-      in = bytes.getReverseReader();
+      if (bytesArray != null) {
+        return new ReverseBytesReader(bytesArray);
+      } else {
+        return bytes.getReverseReader();
+      }
     }
-    return in;
   }
 
   /** Reads bytes stored in an FST. */
@@ -1496,14 +1482,9 @@
     version = VERSION_CURRENT;
     packed = true;
     this.inputType = inputType;
+    bytesArray = null;
     bytes = new BytesStore(bytesPageBits);
     this.outputs = outputs;
-    NO_OUTPUT = outputs.getNoOutput();
-    
-    // NOTE: bogus because this is only used during
-    // building; we need to break out mutable FST from
-    // immutable
-    allowArrayArcs = false;
   }
 
   /** Expert: creates an FST by packing this one.  This
@@ -1518,7 +1499,7 @@
    *  However, this is not a strict implementation of the
    *  algorithms described in this paper.
    */
-  FST<T> pack(int minInCountDeref, int maxDerefNodes, float acceptableOverheadRatio) throws IOException {
+  FST<T> pack(Builder<T> builder, int minInCountDeref, int maxDerefNodes, float acceptableOverheadRatio) throws IOException {
 
     // NOTE: maxDerefNodes is intentionally int: we cannot
     // support > 2.1B deref nodes
@@ -1538,6 +1519,8 @@
       throw new IllegalArgumentException("this FST was not built with willPackFST=true");
     }
 
+    T NO_OUTPUT = outputs.getNoOutput();
+
     Arc<T> arc = new Arc<>();
 
     final BytesReader r = getBytesReader();
@@ -1574,11 +1557,11 @@
 
     // +1 because node ords start at 1 (0 is reserved as stop node):
     final GrowableWriter newNodeAddress = new GrowableWriter(
-                       PackedInts.bitsRequired(this.bytes.getPosition()), (int) (1 + nodeCount), acceptableOverheadRatio);
+                                                             PackedInts.bitsRequired(builder.bytes.getPosition()), (int) (1 + builder.nodeCount), acceptableOverheadRatio);
 
     // Fill initial coarse guess:
-    for(int node=1;node<=nodeCount;node++) {
-      newNodeAddress.set(node, 1 + this.bytes.getPosition() - nodeAddress.get(node));
+    for(int node=1;node<=builder.nodeCount;node++) {
+      newNodeAddress.set(node, 1 + builder.bytes.getPosition() - nodeAddress.get(node));
     }
 
     int absCount;
@@ -1597,7 +1580,7 @@
       // for assert:
       boolean negDelta = false;
 
-      fst = new FST<>(inputType, outputs, bytes.getBlockBits());
+      fst = new FST<>(inputType, outputs, builder.bytes.getBlockBits());
       
       final BytesStore writer = fst.bytes;
 
@@ -1604,10 +1587,6 @@
       // Skip 0 byte since 0 is reserved target:
       writer.writeByte((byte) 0);
 
-      fst.arcWithOutputCount = 0;
-      fst.nodeCount = 0;
-      fst.arcCount = 0;
-
       absCount = deltaCount = topCount = nextCount = 0;
 
       int changedCount = 0;
@@ -1619,8 +1598,7 @@
       // Since we re-reverse the bytes, we now write the
       // nodes backwards, so that BIT_TARGET_NEXT is
       // unchanged:
-      for(int node=(int)nodeCount;node>=1;node--) {
-        fst.nodeCount++;
+      for(int node=(int) builder.nodeCount;node>=1;node--) {
         final long address = writer.getPosition();
 
         //System.out.println("  node: " + node + " address=" + address);
@@ -1732,9 +1710,6 @@
 
             if (arc.output != NO_OUTPUT) {
               outputs.write(arc.output, writer);
-              if (!retry) {
-                fst.arcWithOutputCount++;
-              }
             }
             if (arc.nextFinalOutput != NO_OUTPUT) {
               outputs.writeFinalOutput(arc.nextFinalOutput, writer);
@@ -1817,8 +1792,6 @@
         }
 
         negDelta |= anyNegDelta;
-
-        fst.arcCount += nodeArcCount;
       }
 
       if (!changed) {
@@ -1831,7 +1804,6 @@
         // Converged!
         break;
       }
-      //System.out.println("  " + changedCount + " of " + fst.nodeCount + " changed; retry");
     }
 
     long maxAddress = 0;
@@ -1853,10 +1825,6 @@
       fst.setEmptyOutput(emptyOutput);
     }
 
-    assert fst.nodeCount == nodeCount: "fst.nodeCount=" + fst.nodeCount + " nodeCount=" + nodeCount;
-    assert fst.arcCount == arcCount;
-    assert fst.arcWithOutputCount == arcWithOutputCount: "fst.arcWithOutputCount=" + fst.arcWithOutputCount + " arcWithOutputCount=" + arcWithOutputCount;
-
     fst.bytes.finish();
     fst.cacheRootArcs();
 
Index: lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java	(revision 1663077)
+++ lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java	(working copy)
@@ -114,7 +114,7 @@
     return h & Long.MAX_VALUE;
   }
 
-  public long add(Builder.UnCompiledNode<T> nodeIn) throws IOException {
+  public long add(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn) throws IOException {
     //System.out.println("hash: add count=" + count + " vs " + table.size() + " mask=" + mask);
     final long h = hash(nodeIn);
     long pos = h & mask;
@@ -123,7 +123,7 @@
       final long v = table.get(pos);
       if (v == 0) {
         // freeze & add
-        final long node = fst.addNode(nodeIn);
+        final long node = fst.addNode(builder, nodeIn);
         //System.out.println("  now freeze node=" + node);
         assert hash(node) == h : "frozenHash=" + hash(node) + " vs h=" + h;
         count++;
Index: lucene/core/src/test/org/apache/lucene/util/fst/Test2BFST.java
===================================================================
--- lucene/core/src/test/org/apache/lucene/util/fst/Test2BFST.java	(revision 1663077)
+++ lucene/core/src/test/org/apache/lucene/util/fst/Test2BFST.java	(working copy)
@@ -69,9 +69,9 @@
           b.add(input2, NO_OUTPUT);
           count++;
           if (count % 100000 == 0) {
-            System.out.println(count + ": " + b.fstRamBytesUsed() + " bytes; " + b.getTotStateCount() + " nodes");
+            System.out.println(count + ": " + b.fstRamBytesUsed() + " bytes; " + b.getNodeCount() + " nodes");
           }
-          if (b.getTotStateCount() > Integer.MAX_VALUE + 100L * 1024 * 1024) {
+          if (b.getNodeCount() > Integer.MAX_VALUE + 100L * 1024 * 1024) {
             break;
           }
           nextInput(r, ints2);
@@ -80,7 +80,7 @@
         FST<Object> fst = b.finish();
 
         for(int verify=0;verify<2;verify++) {
-          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + fst.getNodeCount() + "; arcCount=" + fst.getArcCount() + "]");
+          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + b.getNodeCount() + "; arcCount=" + b.getArcCount() + "]");
 
           Arrays.fill(ints2, 0);
           r = new Random(seed);
@@ -161,7 +161,7 @@
         FST<BytesRef> fst = b.finish();
         for(int verify=0;verify<2;verify++) {
 
-          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + fst.getNodeCount() + "; arcCount=" + fst.getArcCount() + "]");
+          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + b.getNodeCount() + "; arcCount=" + b.getArcCount() + "]");
 
           r = new Random(seed);
           Arrays.fill(ints, 0);
@@ -239,7 +239,7 @@
 
         for(int verify=0;verify<2;verify++) {
 
-          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + fst.getNodeCount() + "; arcCount=" + fst.getArcCount() + "]");
+          System.out.println("\nTEST: now verify [fst size=" + fst.ramBytesUsed() + "; nodeCount=" + b.getNodeCount() + "; arcCount=" + b.getArcCount() + "]");
 
           Arrays.fill(ints, 0);
 
Index: lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java
===================================================================
--- lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java	(revision 1663077)
+++ lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java	(working copy)
@@ -133,10 +133,11 @@
         for(IntsRef term : terms2) {
           pairs.add(new FSTTester.InputOutput<>(term, NO_OUTPUT));
         }
-        FST<Object> fst = new FSTTester<>(random(), dir, inputMode, pairs, outputs, false).doTest(0, 0, false);
+        FSTTester<Object> tester = new FSTTester<>(random(), dir, inputMode, pairs, outputs, false);
+        FST<Object> fst = tester.doTest(0, 0, false);
         assertNotNull(fst);
-        assertEquals(22, fst.getNodeCount());
-        assertEquals(27, fst.getArcCount());
+        assertEquals(22, tester.nodeCount);
+        assertEquals(27, tester.arcCount);
       }
 
       // FST ord pos int
@@ -146,10 +147,11 @@
         for(int idx=0;idx<terms2.length;idx++) {
           pairs.add(new FSTTester.InputOutput<>(terms2[idx], (long) idx));
         }
-        final FST<Long> fst = new FSTTester<>(random(), dir, inputMode, pairs, outputs, true).doTest(0, 0, false);
+        FSTTester<Long> tester = new FSTTester<>(random(), dir, inputMode, pairs, outputs, true);
+        final FST<Long> fst = tester.doTest(0, 0, false);
         assertNotNull(fst);
-        assertEquals(22, fst.getNodeCount());
-        assertEquals(27, fst.getArcCount());
+        assertEquals(22, tester.nodeCount);
+        assertEquals(27, tester.arcCount);
       }
 
       // FST byte sequence ord
@@ -161,10 +163,11 @@
           final BytesRef output = random().nextInt(30) == 17 ? NO_OUTPUT : new BytesRef(Integer.toString(idx));
           pairs.add(new FSTTester.InputOutput<>(terms2[idx], output));
         }
-        final FST<BytesRef> fst = new FSTTester<>(random(), dir, inputMode, pairs, outputs, false).doTest(0, 0, false);
+        FSTTester<BytesRef> tester = new FSTTester<>(random(), dir, inputMode, pairs, outputs, false);
+        final FST<BytesRef> fst = tester.doTest(0, 0, false);
         assertNotNull(fst);
-        assertEquals(24, fst.getNodeCount());
-        assertEquals(30, fst.getArcCount());
+        assertEquals(24, tester.nodeCount);
+        assertEquals(30, tester.arcCount);
       }
     }
   }
@@ -381,7 +384,7 @@
       }
       FST<Long> fst = builder.finish();
       if (VERBOSE) {
-        System.out.println("FST: " + docCount + " docs; " + ord + " terms; " + fst.getNodeCount() + " nodes; " + fst.getArcCount() + " arcs;" + " " + fst.ramBytesUsed() + " bytes");
+        System.out.println("FST: " + docCount + " docs; " + ord + " terms; " + builder.getNodeCount() + " nodes; " + builder.getArcCount() + " arcs;" + " " + fst.ramBytesUsed() + " bytes");
       }
 
       if (ord > 0) {
@@ -518,8 +521,8 @@
           return;
         }
 
-        System.out.println(ord + " terms; " + fst.getNodeCount() + " nodes; " + fst.getArcCount() + " arcs; " + fst.getArcWithOutputCount() + " arcs w/ output; tot size " + fst.ramBytesUsed());
-        if (fst.getNodeCount() < 100) {
+        System.out.println(ord + " terms; " + builder.getNodeCount() + " nodes; " + builder.getArcCount() + " arcs; tot size " + fst.ramBytesUsed());
+        if (builder.getNodeCount() < 100) {
           Writer w = Files.newBufferedWriter(Paths.get("out.dot"), StandardCharsets.UTF_8);
           Util.toDot(fst, w, false, false);
           w.close();
@@ -1153,7 +1156,8 @@
     final Long nothing = outputs.getNoOutput();
     final Builder<Long> b = new Builder<>(FST.INPUT_TYPE.BYTE1, outputs);
 
-    final FST<Long> fst = new FST<>(FST.INPUT_TYPE.BYTE1, outputs, false, PackedInts.COMPACT, true, 15);
+    //final FST<Long> fst = new FST<>(FST.INPUT_TYPE.BYTE1, outputs, false, PackedInts.COMPACT, 15);
+    final FST<Long> fst = b.fst;
 
     final Builder.UnCompiledNode<Long> rootNode = new Builder.UnCompiledNode<>(b, 0);
 
@@ -1163,7 +1167,7 @@
       node.isFinal = true;
       rootNode.addArc('a', node);
       final Builder.CompiledNode frozen = new Builder.CompiledNode();
-      frozen.node = fst.addNode(node);
+      frozen.node = fst.addNode(b, node);
       rootNode.arcs[0].nextFinalOutput = 17L;
       rootNode.arcs[0].isFinal = true;
       rootNode.arcs[0].output = nothing;
@@ -1175,13 +1179,13 @@
       final Builder.UnCompiledNode<Long> node = new Builder.UnCompiledNode<>(b, 0);
       rootNode.addArc('b', node);
       final Builder.CompiledNode frozen = new Builder.CompiledNode();
-      frozen.node = fst.addNode(node);
+      frozen.node = fst.addNode(b, node);
       rootNode.arcs[1].nextFinalOutput = nothing;
       rootNode.arcs[1].output = 42L;
       rootNode.arcs[1].target = frozen;
     }
 
-    fst.finish(fst.addNode(rootNode));
+    fst.finish(fst.addNode(b, rootNode));
 
     StringWriter w = new StringWriter();
     //Writer w = new OutputStreamWriter(new FileOutputStream("/x/tmp3/out.dot"));
Index: lucene/test-framework/src/java/org/apache/lucene/index/BaseFieldInfoFormatTestCase.java
===================================================================
--- lucene/test-framework/src/java/org/apache/lucene/index/BaseFieldInfoFormatTestCase.java	(revision 1663077)
+++ lucene/test-framework/src/java/org/apache/lucene/index/BaseFieldInfoFormatTestCase.java	(working copy)
@@ -188,4 +188,21 @@
   public void testRamBytesUsed() throws IOException {
     assumeTrue("not applicable for this format", true);
   }
+
+  // LUCENE-6199: FI.attributes must never be null
+  public void testNonNullAttributes() throws Exception {
+    Directory dir = newDirectory();
+    Codec codec = getCodec();
+    SegmentInfo segmentInfo = newSegmentInfo(dir, "_123");
+    FieldInfos.Builder builder = new FieldInfos.Builder();
+    FieldInfo fi = builder.getOrAdd("field");
+    fi.setIndexOptions(TextField.TYPE_STORED.indexOptions());
+    addAttributes(fi);
+    codec.fieldInfosFormat().write(dir, segmentInfo, "", builder.finish(), IOContext.DEFAULT);
+    FieldInfos infos = codec.fieldInfosFormat().read(dir, segmentInfo, "", IOContext.DEFAULT);
+    FieldInfo info = infos.fieldInfo("field");
+    assertNotNull(info);
+    assertNotNull(info.attributes());
+    dir.close();
+  }
 }
Index: lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java
===================================================================
--- lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java	(revision 1663077)
+++ lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java	(working copy)
@@ -59,6 +59,8 @@
   final Outputs<T> outputs;
   final Directory dir;
   final boolean doReverseLookup;
+  long nodeCount;
+  long arcCount;
 
   public FSTTester(Random random, Directory dir, int inputMode, List<InputOutput<T>> pairs, Outputs<T> outputs, boolean doReverseLookup) {
     this.random = random;
@@ -331,7 +333,7 @@
       if (fst == null) {
         System.out.println("  fst has 0 nodes (fully pruned)");
       } else {
-        System.out.println("  fst has " + fst.getNodeCount() + " nodes and " + fst.getArcCount() + " arcs");
+        System.out.println("  fst has " + builder.getNodeCount() + " nodes and " + builder.getArcCount() + " arcs");
       }
     }
 
@@ -341,6 +343,9 @@
       verifyPruned(inputMode, fst, prune1, prune2);
     }
 
+    nodeCount = builder.getNodeCount();
+    arcCount = builder.getArcCount();
+
     return fst;
   }
 
