View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.notes;
45  
46  import static org.eclipse.jgit.lib.FileMode.TREE;
47  
48  import java.io.IOException;
49  import java.util.Iterator;
50  import java.util.NoSuchElementException;
51  
52  import org.eclipse.jgit.lib.AbbreviatedObjectId;
53  import org.eclipse.jgit.lib.AnyObjectId;
54  import org.eclipse.jgit.lib.MutableObjectId;
55  import org.eclipse.jgit.lib.ObjectId;
56  import org.eclipse.jgit.lib.ObjectInserter;
57  import org.eclipse.jgit.lib.ObjectReader;
58  import org.eclipse.jgit.lib.TreeFormatter;
59  
60  /**
61   * A note tree holding only note subtrees, each named using a 2 digit hex name.
62   *
63   * The fanout buckets/trees contain on average 256 subtrees, naming the subtrees
64   * by a slice of the ObjectId contained within them, from "00" through "ff".
65   *
66   * Each fanout bucket has a {@link #prefixLen} that defines how many digits it
67   * skips in an ObjectId before it gets to the digits matching {@link #table}.
68   *
69   * The root tree has {@code prefixLen == 0}, and thus does not skip any digits.
70   * For ObjectId "c0ffee...", the note (if it exists) will be stored within the
71   * bucket {@code table[0xc0]}.
72   *
73   * The first level tree has {@code prefixLen == 2}, and thus skips the first two
74   * digits. For the same example "c0ffee..." object, its note would be found
75   * within the {@code table[0xff]} bucket (as first 2 digits "c0" are skipped).
76   *
77   * Each subtree is loaded on-demand, reducing startup latency for reads that
78   * only need to examine a few objects. However, due to the rather uniform
79   * distribution of the SHA-1 hash that is used for ObjectIds, accessing 256
80   * objects is very likely to load all of the subtrees into memory.
81   *
82   * A FanoutBucket must be parsed from a tree object by {@link NoteParser}.
83   */
84  class FanoutBucket extends InMemoryNoteBucket {
85  	/**
86  	 * Fan-out table similar to the PackIndex structure.
87  	 *
88  	 * Notes for an object are stored within the sub-bucket that is held here as
89  	 * {@code table[ objectId.getByte( prefixLen / 2 ) ]}. If the slot is null
90  	 * there are no notes with that prefix.
91  	 */
92  	private final NoteBucket[] table;
93  
94  	/** Number of non-null slots in {@link #table}. */
95  	private int cnt;
96  
97  	FanoutBucket(int prefixLen) {
98  		super(prefixLen);
99  		table = new NoteBucket[256];
100 	}
101 
102 	void setBucket(int cell, ObjectId id) {
103 		table[cell] = new LazyNoteBucket(id);
104 		cnt++;
105 	}
106 
107 	void setBucket(int cell, InMemoryNoteBucket bucket) {
108 		table[cell] = bucket;
109 		cnt++;
110 	}
111 
112 	@Override
113 	Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
114 		NoteBucket b = table[cell(objId)];
115 		return b != null ? b.getNote(objId, or) : null;
116 
117 	}
118 
119 	NoteBucket getBucket(int cell) {
120 		return table[cell];
121 	}
122 
123 	static InMemoryNoteBucket loadIfLazy(NoteBucket b, AnyObjectId prefix,
124 			ObjectReader or) throws IOException {
125 		if (b == null)
126 			return null;
127 		if (b instanceof InMemoryNoteBucket)
128 			return (InMemoryNoteBucket) b;
129 		return ((LazyNoteBucket) b).load(prefix, or);
130 	}
131 
132 	@Override
133 	Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
134 			throws IOException {
135 		final MutableObjectId id = new MutableObjectId();
136 		id.fromObjectId(objId);
137 
138 		return new Iterator<Note>() {
139 			private int cell;
140 
141 			private Iterator<Note> itr;
142 
143 			@Override
144 			public boolean hasNext() {
145 				if (itr != null && itr.hasNext())
146 					return true;
147 
148 				for (; cell < table.length; cell++) {
149 					NoteBucket b = table[cell];
150 					if (b == null)
151 						continue;
152 
153 					try {
154 						id.setByte(prefixLen >> 1, cell);
155 						itr = b.iterator(id, reader);
156 					} catch (IOException err) {
157 						throw new RuntimeException(err);
158 					}
159 
160 					if (itr.hasNext()) {
161 						cell++;
162 						return true;
163 					}
164 				}
165 				return false;
166 			}
167 
168 			@Override
169 			public Note next() {
170 				if (hasNext())
171 					return itr.next();
172 				else
173 					throw new NoSuchElementException();
174 			}
175 
176 			@Override
177 			public void remove() {
178 				throw new UnsupportedOperationException();
179 			}
180 		};
181 	}
182 
183 	@Override
184 	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
185 		// If most of this fan-out is full, estimate it should still be split.
186 		if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
187 			return 1 + LeafBucket.MAX_SIZE;
188 
189 		// Due to the uniform distribution of ObjectIds, having less nodes full
190 		// indicates a good chance the total number of children below here
191 		// is less than the MAX_SIZE split point. Get a more accurate count.
192 
193 		MutableObjectId id = new MutableObjectId();
194 		id.fromObjectId(noteOn);
195 
196 		int sz = 0;
197 		for (int cell = 0; cell < 256; cell++) {
198 			NoteBucket b = table[cell];
199 			if (b == null)
200 				continue;
201 
202 			id.setByte(prefixLen >> 1, cell);
203 			sz += b.estimateSize(id, or);
204 			if (LeafBucket.MAX_SIZE < sz)
205 				break;
206 		}
207 		return sz;
208 	}
209 
210 	@Override
211 	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
212 			ObjectReader or) throws IOException {
213 		int cell = cell(noteOn);
214 		NoteBucket b = table[cell];
215 
216 		if (b == null) {
217 			if (noteData == null)
218 				return this;
219 
220 			LeafBucket n = new LeafBucket(prefixLen + 2);
221 			table[cell] = n.set(noteOn, noteData, or);
222 			cnt++;
223 			return this;
224 
225 		} else {
226 			NoteBucket n = b.set(noteOn, noteData, or);
227 			if (n == null) {
228 				table[cell] = null;
229 				cnt--;
230 
231 				if (cnt == 0)
232 					return null;
233 
234 				return contractIfTooSmall(noteOn, or);
235 
236 			} else if (n != b) {
237 				table[cell] = n;
238 			}
239 			return this;
240 		}
241 	}
242 
243 	InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or)
244 			throws IOException {
245 		if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
246 			// We are small enough to just contract to a single leaf.
247 			InMemoryNoteBucket r = new LeafBucket(prefixLen);
248 			for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
249 				r = r.append(i.next());
250 			r.nonNotes = nonNotes;
251 			return r;
252 		}
253 
254 		return this;
255 	}
256 
257 	private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6',
258 			'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
259 
260 	@Override
261 	ObjectId writeTree(ObjectInserter inserter) throws IOException {
262 		return inserter.insert(build(true, inserter));
263 	}
264 
265 	@Override
266 	ObjectId getTreeId() {
267 		try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
268 			return f.idFor(build(false, null));
269 		} catch (IOException e) {
270 			// should never happen as we are not inserting
271 			throw new RuntimeException(e);
272 		}
273 	}
274 
275 	private TreeFormatter build(boolean insert, ObjectInserter inserter)
276 			throws IOException {
277 		byte[] nameBuf = new byte[2];
278 		TreeFormatter fmt = new TreeFormatter(treeSize());
279 		NonNoteEntry e = nonNotes;
280 
281 		for (int cell = 0; cell < 256; cell++) {
282 			NoteBucket b = table[cell];
283 			if (b == null)
284 				continue;
285 
286 			nameBuf[0] = hexchar[cell >>> 4];
287 			nameBuf[1] = hexchar[cell & 0x0f];
288 
289 			while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) {
290 				e.format(fmt);
291 				e = e.next;
292 			}
293 
294 			ObjectId id;
295 			if (insert) {
296 				id = b.writeTree(inserter);
297 			} else {
298 				id = b.getTreeId();
299 			}
300 			fmt.append(nameBuf, 0, 2, TREE, id);
301 		}
302 
303 		for (; e != null; e = e.next)
304 			e.format(fmt);
305 		return fmt;
306 	}
307 
308 	private int treeSize() {
309 		int sz = cnt * TreeFormatter.entrySize(TREE, 2);
310 		for (NonNoteEntry e = nonNotes; e != null; e = e.next)
311 			sz += e.treeEntrySize();
312 		return sz;
313 	}
314 
315 	@Override
316 	InMemoryNoteBucket append(Note note) {
317 		int cell = cell(note);
318 		InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];
319 
320 		if (b == null) {
321 			LeafBucket n = new LeafBucket(prefixLen + 2);
322 			table[cell] = n.append(note);
323 			cnt++;
324 
325 		} else {
326 			InMemoryNoteBucket n = b.append(note);
327 			if (n != b)
328 				table[cell] = n;
329 		}
330 		return this;
331 	}
332 
333 	private int cell(AnyObjectId id) {
334 		return id.getByte(prefixLen >> 1);
335 	}
336 
337 	private class LazyNoteBucket extends NoteBucket {
338 		private final ObjectId treeId;
339 
340 		LazyNoteBucket(ObjectId treeId) {
341 			this.treeId = treeId;
342 		}
343 
344 		@Override
345 		Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
346 			return load(objId, or).getNote(objId, or);
347 		}
348 
349 		@Override
350 		Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
351 				throws IOException {
352 			return load(objId, reader).iterator(objId, reader);
353 		}
354 
355 		@Override
356 		int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
357 			return load(objId, or).estimateSize(objId, or);
358 		}
359 
360 		@Override
361 		InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
362 				ObjectReader or) throws IOException {
363 			return load(noteOn, or).set(noteOn, noteData, or);
364 		}
365 
366 		@Override
367 		ObjectId writeTree(ObjectInserter inserter) {
368 			return treeId;
369 		}
370 
371 		@Override
372 		ObjectId getTreeId() {
373 			return treeId;
374 		}
375 
376 		private InMemoryNoteBucket load(AnyObjectId prefix, ObjectReader or)
377 				throws IOException {
378 			AbbreviatedObjectId p = prefix.abbreviate(prefixLen + 2);
379 			InMemoryNoteBucket self = NoteParser.parse(p, treeId, or);
380 			table[cell(prefix)] = self;
381 			return self;
382 		}
383 	}
384 }