FanoutBucket.java
/*
* Copyright (C) 2010, Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.notes;
import static org.eclipse.jgit.lib.FileMode.TREE;
import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.TreeFormatter;
/**
* A note tree holding only note subtrees, each named using a 2 digit hex name.
*
* The fanout buckets/trees contain on average 256 subtrees, naming the subtrees
* by a slice of the ObjectId contained within them, from "00" through "ff".
*
* Each fanout bucket has a {@link #prefixLen} that defines how many digits it
* skips in an ObjectId before it gets to the digits matching {@link #table}.
*
* The root tree has {@code prefixLen == 0}, and thus does not skip any digits.
* For ObjectId "c0ffee...", the note (if it exists) will be stored within the
* bucket {@code table[0xc0]}.
*
* The first level tree has {@code prefixLen == 2}, and thus skips the first two
* digits. For the same example "c0ffee..." object, its note would be found
* within the {@code table[0xff]} bucket (as first 2 digits "c0" are skipped).
*
* Each subtree is loaded on-demand, reducing startup latency for reads that
* only need to examine a few objects. However, due to the rather uniform
* distribution of the SHA-1 hash that is used for ObjectIds, accessing 256
* objects is very likely to load all of the subtrees into memory.
*
* A FanoutBucket must be parsed from a tree object by {@link NoteParser}.
*/
class FanoutBucket extends InMemoryNoteBucket {
/**
* Fan-out table similar to the PackIndex structure.
*
* Notes for an object are stored within the sub-bucket that is held here as
* {@code table[ objectId.getByte( prefixLen / 2 ) ]}. If the slot is null
* there are no notes with that prefix.
*/
private final NoteBucket[] table;
/** Number of non-null slots in {@link #table}. */
private int cnt;
FanoutBucket(int prefixLen) {
super(prefixLen);
table = new NoteBucket[256];
}
void setBucket(int cell, ObjectId id) {
table[cell] = new LazyNoteBucket(id);
cnt++;
}
void setBucket(int cell, InMemoryNoteBucket bucket) {
table[cell] = bucket;
cnt++;
}
@Override
Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
NoteBucket b = table[cell(objId)];
return b != null ? b.getNote(objId, or) : null;
}
NoteBucket getBucket(int cell) {
return table[cell];
}
static InMemoryNoteBucket loadIfLazy(NoteBucket b, AnyObjectId prefix,
ObjectReader or) throws IOException {
if (b == null)
return null;
if (b instanceof InMemoryNoteBucket)
return (InMemoryNoteBucket) b;
return ((LazyNoteBucket) b).load(prefix, or);
}
@Override
Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
throws IOException {
final MutableObjectId id = new MutableObjectId();
id.fromObjectId(objId);
return new Iterator<>() {
private int cell;
private Iterator<Note> itr;
@Override
public boolean hasNext() {
if (itr != null && itr.hasNext())
return true;
for (; cell < table.length; cell++) {
NoteBucket b = table[cell];
if (b == null)
continue;
try {
id.setByte(prefixLen >> 1, cell);
itr = b.iterator(id, reader);
} catch (IOException err) {
throw new RuntimeException(err);
}
if (itr.hasNext()) {
cell++;
return true;
}
}
return false;
}
@Override
public Note next() {
if (hasNext()) {
return itr.next();
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
// If most of this fan-out is full, estimate it should still be split.
if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
return 1 + LeafBucket.MAX_SIZE;
// Due to the uniform distribution of ObjectIds, having less nodes full
// indicates a good chance the total number of children below here
// is less than the MAX_SIZE split point. Get a more accurate count.
MutableObjectId id = new MutableObjectId();
id.fromObjectId(noteOn);
int sz = 0;
for (int cell = 0; cell < 256; cell++) {
NoteBucket b = table[cell];
if (b == null)
continue;
id.setByte(prefixLen >> 1, cell);
sz += b.estimateSize(id, or);
if (LeafBucket.MAX_SIZE < sz)
break;
}
return sz;
}
@Override
InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
ObjectReader or) throws IOException {
int cell = cell(noteOn);
NoteBucket b = table[cell];
if (b == null) {
if (noteData == null) {
return this;
}
LeafBucket n = new LeafBucket(prefixLen + 2);
table[cell] = n.set(noteOn, noteData, or);
cnt++;
return this;
}
NoteBucket n = b.set(noteOn, noteData, or);
if (n == null) {
table[cell] = null;
cnt--;
if (cnt == 0) {
return null;
}
return contractIfTooSmall(noteOn, or);
} else if (n != b) {
table[cell] = n;
}
return this;
}
InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or)
throws IOException {
if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
// We are small enough to just contract to a single leaf.
InMemoryNoteBucket r = new LeafBucket(prefixLen);
for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
r = r.append(i.next());
r.nonNotes = nonNotes;
return r;
}
return this;
}
private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
@Override
ObjectId writeTree(ObjectInserter inserter) throws IOException {
return inserter.insert(build(true, inserter));
}
@Override
ObjectId getTreeId() {
try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
return f.idFor(build(false, null));
} catch (IOException e) {
// should never happen as we are not inserting
throw new RuntimeException(e);
}
}
private TreeFormatter build(boolean insert, ObjectInserter inserter)
throws IOException {
byte[] nameBuf = new byte[2];
TreeFormatter fmt = new TreeFormatter(treeSize());
NonNoteEntry e = nonNotes;
for (int cell = 0; cell < 256; cell++) {
NoteBucket b = table[cell];
if (b == null)
continue;
nameBuf[0] = hexchar[cell >>> 4];
nameBuf[1] = hexchar[cell & 0x0f];
while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) {
e.format(fmt);
e = e.next;
}
ObjectId id;
if (insert) {
id = b.writeTree(inserter);
} else {
id = b.getTreeId();
}
fmt.append(nameBuf, 0, 2, TREE, id);
}
for (; e != null; e = e.next)
e.format(fmt);
return fmt;
}
private int treeSize() {
int sz = cnt * TreeFormatter.entrySize(TREE, 2);
for (NonNoteEntry e = nonNotes; e != null; e = e.next)
sz += e.treeEntrySize();
return sz;
}
@Override
InMemoryNoteBucket append(Note note) {
int cell = cell(note);
InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];
if (b == null) {
LeafBucket n = new LeafBucket(prefixLen + 2);
table[cell] = n.append(note);
cnt++;
} else {
InMemoryNoteBucket n = b.append(note);
if (n != b)
table[cell] = n;
}
return this;
}
private int cell(AnyObjectId id) {
return id.getByte(prefixLen >> 1);
}
private class LazyNoteBucket extends NoteBucket {
private final ObjectId treeId;
LazyNoteBucket(ObjectId treeId) {
this.treeId = treeId;
}
@Override
Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
return load(objId, or).getNote(objId, or);
}
@Override
Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
throws IOException {
return load(objId, reader).iterator(objId, reader);
}
@Override
int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
return load(objId, or).estimateSize(objId, or);
}
@Override
InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
ObjectReader or) throws IOException {
return load(noteOn, or).set(noteOn, noteData, or);
}
@Override
ObjectId writeTree(ObjectInserter inserter) {
return treeId;
}
@Override
ObjectId getTreeId() {
return treeId;
}
private InMemoryNoteBucket load(AnyObjectId prefix, ObjectReader or)
throws IOException {
AbbreviatedObjectId p = prefix.abbreviate(prefixLen + 2);
InMemoryNoteBucket self = NoteParser.parse(p, treeId, or);
table[cell(prefix)] = self;
return self;
}
}
}