View Javadoc
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  
11  package org.eclipse.jgit.notes;
12  
13  import static org.eclipse.jgit.lib.FileMode.TREE;
14  
15  import java.io.IOException;
16  import java.util.Iterator;
17  import java.util.NoSuchElementException;
18  
19  import org.eclipse.jgit.lib.AbbreviatedObjectId;
20  import org.eclipse.jgit.lib.AnyObjectId;
21  import org.eclipse.jgit.lib.MutableObjectId;
22  import org.eclipse.jgit.lib.ObjectId;
23  import org.eclipse.jgit.lib.ObjectInserter;
24  import org.eclipse.jgit.lib.ObjectReader;
25  import org.eclipse.jgit.lib.TreeFormatter;
26  
27  /**
28   * A note tree holding only note subtrees, each named using a 2 digit hex name.
29   *
30   * The fanout buckets/trees contain on average 256 subtrees, naming the subtrees
31   * by a slice of the ObjectId contained within them, from "00" through "ff".
32   *
33   * Each fanout bucket has a {@link #prefixLen} that defines how many digits it
34   * skips in an ObjectId before it gets to the digits matching {@link #table}.
35   *
36   * The root tree has {@code prefixLen == 0}, and thus does not skip any digits.
37   * For ObjectId "c0ffee...", the note (if it exists) will be stored within the
38   * bucket {@code table[0xc0]}.
39   *
40   * The first level tree has {@code prefixLen == 2}, and thus skips the first two
41   * digits. For the same example "c0ffee..." object, its note would be found
42   * within the {@code table[0xff]} bucket (as first 2 digits "c0" are skipped).
43   *
44   * Each subtree is loaded on-demand, reducing startup latency for reads that
45   * only need to examine a few objects. However, due to the rather uniform
46   * distribution of the SHA-1 hash that is used for ObjectIds, accessing 256
47   * objects is very likely to load all of the subtrees into memory.
48   *
49   * A FanoutBucket must be parsed from a tree object by {@link NoteParser}.
50   */
51  class FanoutBucket extends InMemoryNoteBucket {
52  	/**
53  	 * Fan-out table similar to the PackIndex structure.
54  	 *
55  	 * Notes for an object are stored within the sub-bucket that is held here as
56  	 * {@code table[ objectId.getByte( prefixLen / 2 ) ]}. If the slot is null
57  	 * there are no notes with that prefix.
58  	 */
59  	private final NoteBucket[] table;
60  
61  	/** Number of non-null slots in {@link #table}. */
62  	private int cnt;
63  
64  	FanoutBucket(int prefixLen) {
65  		super(prefixLen);
66  		table = new NoteBucket[256];
67  	}
68  
69  	void setBucket(int cell, ObjectId id) {
70  		table[cell] = new LazyNoteBucket(id);
71  		cnt++;
72  	}
73  
74  	void setBucket(int cell, InMemoryNoteBucket bucket) {
75  		table[cell] = bucket;
76  		cnt++;
77  	}
78  
79  	@Override
80  	Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
81  		NoteBucket b = table[cell(objId)];
82  		return b != null ? b.getNote(objId, or) : null;
83  
84  	}
85  
86  	NoteBucket getBucket(int cell) {
87  		return table[cell];
88  	}
89  
90  	static InMemoryNoteBucket loadIfLazy(NoteBucket b, AnyObjectId prefix,
91  			ObjectReader or) throws IOException {
92  		if (b == null)
93  			return null;
94  		if (b instanceof InMemoryNoteBucket)
95  			return (InMemoryNoteBucket) b;
96  		return ((LazyNoteBucket) b).load(prefix, or);
97  	}
98  
99  	@Override
100 	Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
101 			throws IOException {
102 		final MutableObjectId id = new MutableObjectId();
103 		id.fromObjectId(objId);
104 
105 		return new Iterator<>() {
106 			private int cell;
107 
108 			private Iterator<Note> itr;
109 
110 			@Override
111 			public boolean hasNext() {
112 				if (itr != null && itr.hasNext())
113 					return true;
114 
115 				for (; cell < table.length; cell++) {
116 					NoteBucket b = table[cell];
117 					if (b == null)
118 						continue;
119 
120 					try {
121 						id.setByte(prefixLen >> 1, cell);
122 						itr = b.iterator(id, reader);
123 					} catch (IOException err) {
124 						throw new RuntimeException(err);
125 					}
126 
127 					if (itr.hasNext()) {
128 						cell++;
129 						return true;
130 					}
131 				}
132 				return false;
133 			}
134 
135 			@Override
136 			public Note next() {
137 				if (hasNext()) {
138 					return itr.next();
139 				}
140 				throw new NoSuchElementException();
141 			}
142 
143 			@Override
144 			public void remove() {
145 				throw new UnsupportedOperationException();
146 			}
147 		};
148 	}
149 
150 	@Override
151 	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
152 		// If most of this fan-out is full, estimate it should still be split.
153 		if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
154 			return 1 + LeafBucket.MAX_SIZE;
155 
156 		// Due to the uniform distribution of ObjectIds, having less nodes full
157 		// indicates a good chance the total number of children below here
158 		// is less than the MAX_SIZE split point. Get a more accurate count.
159 
160 		MutableObjectId id = new MutableObjectId();
161 		id.fromObjectId(noteOn);
162 
163 		int sz = 0;
164 		for (int cell = 0; cell < 256; cell++) {
165 			NoteBucket b = table[cell];
166 			if (b == null)
167 				continue;
168 
169 			id.setByte(prefixLen >> 1, cell);
170 			sz += b.estimateSize(id, or);
171 			if (LeafBucket.MAX_SIZE < sz)
172 				break;
173 		}
174 		return sz;
175 	}
176 
177 	@Override
178 	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
179 			ObjectReader or) throws IOException {
180 		int cell = cell(noteOn);
181 		NoteBucket b = table[cell];
182 
183 		if (b == null) {
184 			if (noteData == null) {
185 				return this;
186 			}
187 
188 			LeafBucket n = new LeafBucket(prefixLen + 2);
189 			table[cell] = n.set(noteOn, noteData, or);
190 			cnt++;
191 			return this;
192 
193 		}
194 		NoteBucket n = b.set(noteOn, noteData, or);
195 		if (n == null) {
196 			table[cell] = null;
197 			cnt--;
198 
199 			if (cnt == 0) {
200 				return null;
201 			}
202 
203 			return contractIfTooSmall(noteOn, or);
204 
205 		} else if (n != b) {
206 			table[cell] = n;
207 		}
208 		return this;
209 	}
210 
211 	InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or)
212 			throws IOException {
213 		if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
214 			// We are small enough to just contract to a single leaf.
215 			InMemoryNoteBucket r = new LeafBucket(prefixLen);
216 			for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
217 				r = r.append(i.next());
218 			r.nonNotes = nonNotes;
219 			return r;
220 		}
221 
222 		return this;
223 	}
224 
225 	private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6',
226 			'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
227 
228 	@Override
229 	ObjectId writeTree(ObjectInserter inserter) throws IOException {
230 		return inserter.insert(build(true, inserter));
231 	}
232 
233 	@Override
234 	ObjectId getTreeId() {
235 		try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
236 			return f.idFor(build(false, null));
237 		} catch (IOException e) {
238 			// should never happen as we are not inserting
239 			throw new RuntimeException(e);
240 		}
241 	}
242 
243 	private TreeFormatter build(boolean insert, ObjectInserter inserter)
244 			throws IOException {
245 		byte[] nameBuf = new byte[2];
246 		TreeFormatter fmt = new TreeFormatter(treeSize());
247 		NonNoteEntry e = nonNotes;
248 
249 		for (int cell = 0; cell < 256; cell++) {
250 			NoteBucket b = table[cell];
251 			if (b == null)
252 				continue;
253 
254 			nameBuf[0] = hexchar[cell >>> 4];
255 			nameBuf[1] = hexchar[cell & 0x0f];
256 
257 			while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) {
258 				e.format(fmt);
259 				e = e.next;
260 			}
261 
262 			ObjectId id;
263 			if (insert) {
264 				id = b.writeTree(inserter);
265 			} else {
266 				id = b.getTreeId();
267 			}
268 			fmt.append(nameBuf, 0, 2, TREE, id);
269 		}
270 
271 		for (; e != null; e = e.next)
272 			e.format(fmt);
273 		return fmt;
274 	}
275 
276 	private int treeSize() {
277 		int sz = cnt * TreeFormatter.entrySize(TREE, 2);
278 		for (NonNoteEntry e = nonNotes; e != null; e = e.next)
279 			sz += e.treeEntrySize();
280 		return sz;
281 	}
282 
283 	@Override
284 	InMemoryNoteBucket append(Note note) {
285 		int cell = cell(note);
286 		InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];
287 
288 		if (b == null) {
289 			LeafBucket n = new LeafBucket(prefixLen + 2);
290 			table[cell] = n.append(note);
291 			cnt++;
292 
293 		} else {
294 			InMemoryNoteBucket n = b.append(note);
295 			if (n != b)
296 				table[cell] = n;
297 		}
298 		return this;
299 	}
300 
301 	private int cell(AnyObjectId id) {
302 		return id.getByte(prefixLen >> 1);
303 	}
304 
305 	private class LazyNoteBucket extends NoteBucket {
306 		private final ObjectId treeId;
307 
308 		LazyNoteBucket(ObjectId treeId) {
309 			this.treeId = treeId;
310 		}
311 
312 		@Override
313 		Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
314 			return load(objId, or).getNote(objId, or);
315 		}
316 
317 		@Override
318 		Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
319 				throws IOException {
320 			return load(objId, reader).iterator(objId, reader);
321 		}
322 
323 		@Override
324 		int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
325 			return load(objId, or).estimateSize(objId, or);
326 		}
327 
328 		@Override
329 		InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
330 				ObjectReader or) throws IOException {
331 			return load(noteOn, or).set(noteOn, noteData, or);
332 		}
333 
334 		@Override
335 		ObjectId writeTree(ObjectInserter inserter) {
336 			return treeId;
337 		}
338 
339 		@Override
340 		ObjectId getTreeId() {
341 			return treeId;
342 		}
343 
344 		private InMemoryNoteBucket load(AnyObjectId prefix, ObjectReader or)
345 				throws IOException {
346 			AbbreviatedObjectId p = prefix.abbreviate(prefixLen + 2);
347 			InMemoryNoteBucket self = NoteParser.parse(p, treeId, or);
348 			table[cell(prefix)] = self;
349 			return self;
350 		}
351 	}
352 }