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;
		}
	}
}