FanoutBucket.java

  1. /*
  2.  * Copyright (C) 2010, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.notes;

  11. import static org.eclipse.jgit.lib.FileMode.TREE;

  12. import java.io.IOException;
  13. import java.util.Iterator;
  14. import java.util.NoSuchElementException;

  15. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  16. import org.eclipse.jgit.lib.AnyObjectId;
  17. import org.eclipse.jgit.lib.MutableObjectId;
  18. import org.eclipse.jgit.lib.ObjectId;
  19. import org.eclipse.jgit.lib.ObjectInserter;
  20. import org.eclipse.jgit.lib.ObjectReader;
  21. import org.eclipse.jgit.lib.TreeFormatter;

  22. /**
  23.  * A note tree holding only note subtrees, each named using a 2 digit hex name.
  24.  *
  25.  * The fanout buckets/trees contain on average 256 subtrees, naming the subtrees
  26.  * by a slice of the ObjectId contained within them, from "00" through "ff".
  27.  *
  28.  * Each fanout bucket has a {@link #prefixLen} that defines how many digits it
  29.  * skips in an ObjectId before it gets to the digits matching {@link #table}.
  30.  *
  31.  * The root tree has {@code prefixLen == 0}, and thus does not skip any digits.
  32.  * For ObjectId "c0ffee...", the note (if it exists) will be stored within the
  33.  * bucket {@code table[0xc0]}.
  34.  *
  35.  * The first level tree has {@code prefixLen == 2}, and thus skips the first two
  36.  * digits. For the same example "c0ffee..." object, its note would be found
  37.  * within the {@code table[0xff]} bucket (as first 2 digits "c0" are skipped).
  38.  *
  39.  * Each subtree is loaded on-demand, reducing startup latency for reads that
  40.  * only need to examine a few objects. However, due to the rather uniform
  41.  * distribution of the SHA-1 hash that is used for ObjectIds, accessing 256
  42.  * objects is very likely to load all of the subtrees into memory.
  43.  *
  44.  * A FanoutBucket must be parsed from a tree object by {@link NoteParser}.
  45.  */
  46. class FanoutBucket extends InMemoryNoteBucket {
  47.     /**
  48.      * Fan-out table similar to the PackIndex structure.
  49.      *
  50.      * Notes for an object are stored within the sub-bucket that is held here as
  51.      * {@code table[ objectId.getByte( prefixLen / 2 ) ]}. If the slot is null
  52.      * there are no notes with that prefix.
  53.      */
  54.     private final NoteBucket[] table;

  55.     /** Number of non-null slots in {@link #table}. */
  56.     private int cnt;

  57.     FanoutBucket(int prefixLen) {
  58.         super(prefixLen);
  59.         table = new NoteBucket[256];
  60.     }

  61.     void setBucket(int cell, ObjectId id) {
  62.         table[cell] = new LazyNoteBucket(id);
  63.         cnt++;
  64.     }

  65.     void setBucket(int cell, InMemoryNoteBucket bucket) {
  66.         table[cell] = bucket;
  67.         cnt++;
  68.     }

  69.     @Override
  70.     Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
  71.         NoteBucket b = table[cell(objId)];
  72.         return b != null ? b.getNote(objId, or) : null;

  73.     }

  74.     NoteBucket getBucket(int cell) {
  75.         return table[cell];
  76.     }

  77.     static InMemoryNoteBucket loadIfLazy(NoteBucket b, AnyObjectId prefix,
  78.             ObjectReader or) throws IOException {
  79.         if (b == null)
  80.             return null;
  81.         if (b instanceof InMemoryNoteBucket)
  82.             return (InMemoryNoteBucket) b;
  83.         return ((LazyNoteBucket) b).load(prefix, or);
  84.     }

  85.     @Override
  86.     Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
  87.             throws IOException {
  88.         final MutableObjectId id = new MutableObjectId();
  89.         id.fromObjectId(objId);

  90.         return new Iterator<>() {
  91.             private int cell;

  92.             private Iterator<Note> itr;

  93.             @Override
  94.             public boolean hasNext() {
  95.                 if (itr != null && itr.hasNext())
  96.                     return true;

  97.                 for (; cell < table.length; cell++) {
  98.                     NoteBucket b = table[cell];
  99.                     if (b == null)
  100.                         continue;

  101.                     try {
  102.                         id.setByte(prefixLen >> 1, cell);
  103.                         itr = b.iterator(id, reader);
  104.                     } catch (IOException err) {
  105.                         throw new RuntimeException(err);
  106.                     }

  107.                     if (itr.hasNext()) {
  108.                         cell++;
  109.                         return true;
  110.                     }
  111.                 }
  112.                 return false;
  113.             }

  114.             @Override
  115.             public Note next() {
  116.                 if (hasNext()) {
  117.                     return itr.next();
  118.                 }
  119.                 throw new NoSuchElementException();
  120.             }

  121.             @Override
  122.             public void remove() {
  123.                 throw new UnsupportedOperationException();
  124.             }
  125.         };
  126.     }

  127.     @Override
  128.     int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
  129.         // If most of this fan-out is full, estimate it should still be split.
  130.         if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
  131.             return 1 + LeafBucket.MAX_SIZE;

  132.         // Due to the uniform distribution of ObjectIds, having less nodes full
  133.         // indicates a good chance the total number of children below here
  134.         // is less than the MAX_SIZE split point. Get a more accurate count.

  135.         MutableObjectId id = new MutableObjectId();
  136.         id.fromObjectId(noteOn);

  137.         int sz = 0;
  138.         for (int cell = 0; cell < 256; cell++) {
  139.             NoteBucket b = table[cell];
  140.             if (b == null)
  141.                 continue;

  142.             id.setByte(prefixLen >> 1, cell);
  143.             sz += b.estimateSize(id, or);
  144.             if (LeafBucket.MAX_SIZE < sz)
  145.                 break;
  146.         }
  147.         return sz;
  148.     }

  149.     @Override
  150.     InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
  151.             ObjectReader or) throws IOException {
  152.         int cell = cell(noteOn);
  153.         NoteBucket b = table[cell];

  154.         if (b == null) {
  155.             if (noteData == null) {
  156.                 return this;
  157.             }

  158.             LeafBucket n = new LeafBucket(prefixLen + 2);
  159.             table[cell] = n.set(noteOn, noteData, or);
  160.             cnt++;
  161.             return this;

  162.         }
  163.         NoteBucket n = b.set(noteOn, noteData, or);
  164.         if (n == null) {
  165.             table[cell] = null;
  166.             cnt--;

  167.             if (cnt == 0) {
  168.                 return null;
  169.             }

  170.             return contractIfTooSmall(noteOn, or);

  171.         } else if (n != b) {
  172.             table[cell] = n;
  173.         }
  174.         return this;
  175.     }

  176.     InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or)
  177.             throws IOException {
  178.         if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
  179.             // We are small enough to just contract to a single leaf.
  180.             InMemoryNoteBucket r = new LeafBucket(prefixLen);
  181.             for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
  182.                 r = r.append(i.next());
  183.             r.nonNotes = nonNotes;
  184.             return r;
  185.         }

  186.         return this;
  187.     }

  188.     private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6',
  189.             '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

  190.     @Override
  191.     ObjectId writeTree(ObjectInserter inserter) throws IOException {
  192.         return inserter.insert(build(true, inserter));
  193.     }

  194.     @Override
  195.     ObjectId getTreeId() {
  196.         try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
  197.             return f.idFor(build(false, null));
  198.         } catch (IOException e) {
  199.             // should never happen as we are not inserting
  200.             throw new RuntimeException(e);
  201.         }
  202.     }

  203.     private TreeFormatter build(boolean insert, ObjectInserter inserter)
  204.             throws IOException {
  205.         byte[] nameBuf = new byte[2];
  206.         TreeFormatter fmt = new TreeFormatter(treeSize());
  207.         NonNoteEntry e = nonNotes;

  208.         for (int cell = 0; cell < 256; cell++) {
  209.             NoteBucket b = table[cell];
  210.             if (b == null)
  211.                 continue;

  212.             nameBuf[0] = hexchar[cell >>> 4];
  213.             nameBuf[1] = hexchar[cell & 0x0f];

  214.             while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) {
  215.                 e.format(fmt);
  216.                 e = e.next;
  217.             }

  218.             ObjectId id;
  219.             if (insert) {
  220.                 id = b.writeTree(inserter);
  221.             } else {
  222.                 id = b.getTreeId();
  223.             }
  224.             fmt.append(nameBuf, 0, 2, TREE, id);
  225.         }

  226.         for (; e != null; e = e.next)
  227.             e.format(fmt);
  228.         return fmt;
  229.     }

  230.     private int treeSize() {
  231.         int sz = cnt * TreeFormatter.entrySize(TREE, 2);
  232.         for (NonNoteEntry e = nonNotes; e != null; e = e.next)
  233.             sz += e.treeEntrySize();
  234.         return sz;
  235.     }

  236.     @Override
  237.     InMemoryNoteBucket append(Note note) {
  238.         int cell = cell(note);
  239.         InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];

  240.         if (b == null) {
  241.             LeafBucket n = new LeafBucket(prefixLen + 2);
  242.             table[cell] = n.append(note);
  243.             cnt++;

  244.         } else {
  245.             InMemoryNoteBucket n = b.append(note);
  246.             if (n != b)
  247.                 table[cell] = n;
  248.         }
  249.         return this;
  250.     }

  251.     private int cell(AnyObjectId id) {
  252.         return id.getByte(prefixLen >> 1);
  253.     }

  254.     private class LazyNoteBucket extends NoteBucket {
  255.         private final ObjectId treeId;

  256.         LazyNoteBucket(ObjectId treeId) {
  257.             this.treeId = treeId;
  258.         }

  259.         @Override
  260.         Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
  261.             return load(objId, or).getNote(objId, or);
  262.         }

  263.         @Override
  264.         Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
  265.                 throws IOException {
  266.             return load(objId, reader).iterator(objId, reader);
  267.         }

  268.         @Override
  269.         int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
  270.             return load(objId, or).estimateSize(objId, or);
  271.         }

  272.         @Override
  273.         InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
  274.                 ObjectReader or) throws IOException {
  275.             return load(noteOn, or).set(noteOn, noteData, or);
  276.         }

  277.         @Override
  278.         ObjectId writeTree(ObjectInserter inserter) {
  279.             return treeId;
  280.         }

  281.         @Override
  282.         ObjectId getTreeId() {
  283.             return treeId;
  284.         }

  285.         private InMemoryNoteBucket load(AnyObjectId prefix, ObjectReader or)
  286.                 throws IOException {
  287.             AbbreviatedObjectId p = prefix.abbreviate(prefixLen + 2);
  288.             InMemoryNoteBucket self = NoteParser.parse(p, treeId, or);
  289.             table[cell(prefix)] = self;
  290.             return self;
  291.         }
  292.     }
  293. }