| /* |
| * Copyright (C) 2011, Google Inc. |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.eclipse.jgit.storage.dht; |
| |
| import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; |
| import static org.eclipse.jgit.lib.Constants.OBJ_TREE; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.TimeoutException; |
| import java.util.zip.Inflater; |
| |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException; |
| import org.eclipse.jgit.generated.storage.dht.proto.GitStore.CachedPackInfo; |
| import org.eclipse.jgit.lib.AbbreviatedObjectId; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.AsyncObjectLoaderQueue; |
| import org.eclipse.jgit.lib.AsyncObjectSizeQueue; |
| import org.eclipse.jgit.lib.InflaterCache; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectLoader; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.ProgressMonitor; |
| import org.eclipse.jgit.revwalk.ObjectWalk; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.storage.dht.spi.Context; |
| import org.eclipse.jgit.storage.dht.spi.Database; |
| import org.eclipse.jgit.storage.dht.spi.ObjectIndexTable; |
| import org.eclipse.jgit.storage.pack.CachedPack; |
| import org.eclipse.jgit.storage.pack.ObjectReuseAsIs; |
| import org.eclipse.jgit.storage.pack.ObjectToPack; |
| import org.eclipse.jgit.storage.pack.PackOutputStream; |
| import org.eclipse.jgit.storage.pack.PackWriter; |
| |
| /** |
| * ObjectReader implementation for DHT based repositories. |
| * <p> |
| * This class is public only to expose its unique statistics for runtime |
| * performance reporting. Applications should always prefer to use the more |
| * generic base class, {@link ObjectReader}. |
| */ |
| public class DhtReader extends ObjectReader implements ObjectReuseAsIs { |
| private final DhtRepository repository; |
| |
| private final RepositoryKey repo; |
| |
| private final Database db; |
| |
| private final DhtReaderOptions readerOptions; |
| |
| private final DhtInserterOptions inserterOptions; |
| |
| private final Statistics stats; |
| |
| private final RecentInfoCache recentInfo; |
| |
| private final RecentChunks recentChunks; |
| |
| private final DeltaBaseCache deltaBaseCache; |
| |
| private Collection<CachedPack> cachedPacks; |
| |
| private Inflater inflater; |
| |
| private Prefetcher prefetcher; |
| |
| DhtReader(DhtObjDatabase objdb) { |
| this.repository = objdb.getRepository(); |
| this.repo = objdb.getRepository().getRepositoryKey(); |
| this.db = objdb.getDatabase(); |
| this.readerOptions = objdb.getReaderOptions(); |
| this.inserterOptions = objdb.getInserterOptions(); |
| |
| this.stats = new Statistics(); |
| this.recentInfo = new RecentInfoCache(getOptions()); |
| this.recentChunks = new RecentChunks(this); |
| this.deltaBaseCache = new DeltaBaseCache(this); |
| } |
| |
| /** @return describes how this DhtReader has performed. */ |
| public Statistics getStatistics() { |
| return stats; |
| } |
| |
| Database getDatabase() { |
| return db; |
| } |
| |
| RepositoryKey getRepositoryKey() { |
| return repo; |
| } |
| |
| DhtReaderOptions getOptions() { |
| return readerOptions; |
| } |
| |
| DhtInserterOptions getInserterOptions() { |
| return inserterOptions; |
| } |
| |
| RecentInfoCache getRecentInfoCache() { |
| return recentInfo; |
| } |
| |
| DeltaBaseCache getDeltaBaseCache() { |
| return deltaBaseCache; |
| } |
| |
| Inflater inflater() { |
| if (inflater == null) |
| inflater = InflaterCache.get(); |
| else |
| inflater.reset(); |
| return inflater; |
| } |
| |
| @Override |
| public void release() { |
| recentChunks.clear(); |
| endPrefetch(); |
| |
| InflaterCache.release(inflater); |
| inflater = null; |
| |
| super.release(); |
| } |
| |
| @Override |
| public ObjectReader newReader() { |
| return new DhtReader(repository.getObjectDatabase()); |
| } |
| |
| @Override |
| public boolean has(AnyObjectId objId, int typeHint) throws IOException { |
| if (objId instanceof RefDataUtil.IdWithChunk) |
| return true; |
| |
| if (recentChunks.has(repo, objId)) |
| return true; |
| |
| if (repository.getRefDatabase().findChunk(objId) != null) |
| return true; |
| |
| return !find(objId).isEmpty(); |
| } |
| |
| @Override |
| public ObjectLoader open(AnyObjectId objId, int typeHint) |
| throws MissingObjectException, IncorrectObjectTypeException, |
| IOException { |
| ObjectLoader ldr = recentChunks.open(repo, objId, typeHint); |
| if (ldr != null) |
| return ldr; |
| |
| ChunkAndOffset p = getChunk(objId, typeHint, false); |
| ldr = PackChunk.read(p.chunk, p.offset, this, typeHint); |
| recentChunk(p.chunk); |
| return ldr; |
| } |
| |
| @Override |
| public <T extends ObjectId> AsyncObjectLoaderQueue<T> open( |
| Iterable<T> objectIds, boolean reportMissing) { |
| return new OpenQueue<T>(this, objectIds, reportMissing); |
| } |
| |
| @Override |
| public long getObjectSize(AnyObjectId objectId, int typeHint) |
| throws MissingObjectException, IncorrectObjectTypeException, |
| IOException { |
| for (ObjectInfo info : find(objectId)) |
| return info.getSize(); |
| throw missing(objectId, typeHint); |
| } |
| |
| @Override |
| public <T extends ObjectId> AsyncObjectSizeQueue<T> getObjectSize( |
| Iterable<T> objectIds, boolean reportMissing) { |
| return new SizeQueue<T>(this, objectIds, reportMissing); |
| } |
| |
| @Override |
| public void walkAdviceBeginCommits(RevWalk rw, Collection<RevCommit> roots) |
| throws IOException { |
| endPrefetch(); |
| |
| // Don't assign the prefetcher right away. Delay until its |
| // configured as push might invoke our own methods that may |
| // try to call back into the active prefetcher. |
| // |
| Prefetcher p = new Prefetcher(this, OBJ_COMMIT); |
| p.push(this, roots); |
| prefetcher = p; |
| } |
| |
| @Override |
| public void walkAdviceBeginTrees(ObjectWalk ow, RevCommit min, RevCommit max) |
| throws IOException { |
| endPrefetch(); |
| |
| // Don't assign the prefetcher right away. Delay until its |
| // configured as push might invoke our own methods that may |
| // try to call back into the active prefetcher. |
| // |
| Prefetcher p = new Prefetcher(this, OBJ_TREE); |
| p.push(this, min.getTree(), max.getTree()); |
| prefetcher = p; |
| } |
| |
| @Override |
| public void walkAdviceEnd() { |
| endPrefetch(); |
| } |
| |
| void recentChunk(PackChunk chunk) { |
| recentChunks.put(chunk); |
| } |
| |
| ChunkAndOffset getChunkGently(AnyObjectId objId) { |
| return recentChunks.find(repo, objId); |
| } |
| |
| ChunkAndOffset getChunk(AnyObjectId objId, int typeHint, boolean checkRecent) |
| throws DhtException, MissingObjectException { |
| if (checkRecent) { |
| ChunkAndOffset r = recentChunks.find(repo, objId); |
| if (r != null) |
| return r; |
| } |
| |
| ChunkKey key; |
| if (objId instanceof RefDataUtil.IdWithChunk) |
| key = ((RefDataUtil.IdWithChunk) objId).getChunkKey(); |
| else |
| key = repository.getRefDatabase().findChunk(objId); |
| |
| if (key != null) { |
| PackChunk chunk = load(key); |
| if (chunk != null && chunk.hasIndex()) { |
| int pos = chunk.findOffset(repo, objId); |
| if (0 <= pos) |
| return new ChunkAndOffset(chunk, pos); |
| } |
| |
| // The hint above is stale. Fall through and do a |
| // more exhaustive lookup to find the object. |
| } |
| |
| if (prefetcher != null) { |
| ChunkAndOffset r = prefetcher.find(repo, objId); |
| if (r != null) |
| return r; |
| } |
| |
| for (ObjectInfo link : find(objId)) { |
| PackChunk chunk; |
| |
| if (prefetcher != null) { |
| chunk = prefetcher.get(link.getChunkKey()); |
| if (chunk == null) { |
| chunk = load(link.getChunkKey()); |
| if (chunk == null) |
| continue; |
| if (prefetcher.isType(typeHint)) |
| prefetcher.push(chunk.getMeta()); |
| } |
| } else { |
| chunk = load(link.getChunkKey()); |
| if (chunk == null) |
| continue; |
| } |
| |
| return new ChunkAndOffset(chunk, link.getOffset()); |
| } |
| |
| throw missing(objId, typeHint); |
| } |
| |
| ChunkKey findChunk(AnyObjectId objId) throws DhtException { |
| if (objId instanceof RefDataUtil.IdWithChunk) |
| return ((RefDataUtil.IdWithChunk) objId).getChunkKey(); |
| |
| ChunkKey key = repository.getRefDatabase().findChunk(objId); |
| if (key != null) |
| return key; |
| |
| ChunkAndOffset r = recentChunks.find(repo, objId); |
| if (r != null) |
| return r.chunk.getChunkKey(); |
| |
| for (ObjectInfo link : find(objId)) |
| return link.getChunkKey(); |
| |
| return null; |
| } |
| |
| static MissingObjectException missing(AnyObjectId objId, int typeHint) { |
| ObjectId id = objId.copy(); |
| if (typeHint != OBJ_ANY) |
| return new MissingObjectException(id, typeHint); |
| return new MissingObjectException(id, DhtText.get().objectTypeUnknown); |
| } |
| |
| PackChunk getChunk(ChunkKey key) throws DhtException { |
| PackChunk chunk = recentChunks.get(key); |
| if (chunk != null) |
| return chunk; |
| |
| chunk = load(key); |
| if (chunk != null) |
| return chunk; |
| |
| throw new DhtMissingChunkException(key); |
| } |
| |
| @Override |
| public Collection<ObjectId> resolve(AbbreviatedObjectId id) |
| throws IOException { |
| // Because ObjectIndexKey requires at least 4 leading digits |
| // don't resolve anything that is shorter than 4 digits. |
| // |
| if (id.length() < 4) |
| return Collections.emptySet(); |
| |
| throw new DhtException.TODO("resolve abbreviations"); |
| } |
| |
| public DhtObjectToPack newObjectToPack(RevObject obj) { |
| return new DhtObjectToPack(obj); |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void selectObjectRepresentation(PackWriter packer, |
| ProgressMonitor monitor, Iterable<ObjectToPack> objects) |
| throws IOException, MissingObjectException { |
| Iterable itr = objects; |
| new RepresentationSelector(packer, this, monitor).select(itr); |
| } |
| |
| private void endPrefetch() { |
| prefetcher = null; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void writeObjects(PackOutputStream out, List<ObjectToPack> objects) |
| throws IOException { |
| prefetcher = new Prefetcher(this, 0); |
| try { |
| List itr = objects; |
| new ObjectWriter(this, prefetcher).plan(itr); |
| for (ObjectToPack otp : objects) |
| out.writeObject(otp); |
| } finally { |
| endPrefetch(); |
| } |
| } |
| |
| public void copyObjectAsIs(PackOutputStream out, ObjectToPack otp, |
| boolean validate) throws IOException, |
| StoredObjectRepresentationNotAvailableException { |
| DhtObjectToPack obj = (DhtObjectToPack) otp; |
| try { |
| PackChunk chunk = recentChunks.get(obj.chunk); |
| if (chunk == null) { |
| chunk = prefetcher.get(obj.chunk); |
| if (chunk == null) { |
| // This should never happen during packing, it implies |
| // the fetch plan was incorrect. Unfortunately that can |
| // occur if objects need to be recompressed on the fly. |
| // |
| stats.access(obj.chunk).cntCopyObjectAsIs_PrefetchMiss++; |
| chunk = getChunk(obj.chunk); |
| } |
| if (!chunk.isFragment()) |
| recentChunk(chunk); |
| } |
| chunk.copyObjectAsIs(out, obj, validate, this); |
| } catch (DhtMissingChunkException missingChunk) { |
| stats.access(missingChunk.getChunkKey()).cntCopyObjectAsIs_InvalidChunk++; |
| throw new StoredObjectRepresentationNotAvailableException(otp); |
| } |
| } |
| |
| public Collection<CachedPack> getCachedPacks() throws IOException { |
| if (cachedPacks == null) { |
| Collection<CachedPackInfo> info; |
| Collection<CachedPack> packs; |
| |
| try { |
| info = db.repository().getCachedPacks(repo); |
| } catch (TimeoutException e) { |
| throw new DhtTimeoutException(e); |
| } |
| |
| packs = new ArrayList<CachedPack>(info.size()); |
| for (CachedPackInfo i : info) |
| packs.add(new DhtCachedPack(i)); |
| cachedPacks = packs; |
| } |
| return cachedPacks; |
| } |
| |
| public void copyPackAsIs(PackOutputStream out, CachedPack pack, |
| boolean validate) throws IOException { |
| ((DhtCachedPack) pack).copyAsIs(out, validate, this); |
| } |
| |
| private List<ObjectInfo> find(AnyObjectId obj) throws DhtException { |
| List<ObjectInfo> info = recentInfo.get(obj); |
| if (info != null) |
| return info; |
| |
| stats.cntObjectIndex_Load++; |
| ObjectIndexKey idxKey = ObjectIndexKey.create(repo, obj); |
| Context opt = Context.READ_REPAIR; |
| Sync<Map<ObjectIndexKey, Collection<ObjectInfo>>> sync = Sync.create(); |
| db.objectIndex().get(opt, Collections.singleton(idxKey), sync); |
| try { |
| Collection<ObjectInfo> m; |
| |
| m = sync.get(getOptions().getTimeout()).get(idxKey); |
| if (m == null || m.isEmpty()) |
| return Collections.emptyList(); |
| |
| info = new ArrayList<ObjectInfo>(m); |
| ObjectInfo.sort(info); |
| recentInfo.put(obj, info); |
| return info; |
| } catch (InterruptedException e) { |
| throw new DhtTimeoutException(e); |
| } catch (TimeoutException e) { |
| throw new DhtTimeoutException(e); |
| } |
| } |
| |
| private PackChunk load(ChunkKey chunkKey) throws DhtException { |
| if (0 == stats.access(chunkKey).cntReader_Load++ |
| && readerOptions.isTrackFirstChunkLoad()) |
| stats.access(chunkKey).locReader_Load = new Throwable("first"); |
| Context opt = Context.READ_REPAIR; |
| Sync<Collection<PackChunk.Members>> sync = Sync.create(); |
| db.chunk().get(opt, Collections.singleton(chunkKey), sync); |
| try { |
| Collection<PackChunk.Members> c = sync.get(getOptions() |
| .getTimeout()); |
| if (c.isEmpty()) |
| return null; |
| if (c instanceof List) |
| return ((List<PackChunk.Members>) c).get(0).build(); |
| return c.iterator().next().build(); |
| } catch (InterruptedException e) { |
| throw new DhtTimeoutException(e); |
| } catch (TimeoutException e) { |
| throw new DhtTimeoutException(e); |
| } |
| } |
| |
| static class ChunkAndOffset { |
| final PackChunk chunk; |
| |
| final int offset; |
| |
| ChunkAndOffset(PackChunk chunk, int offset) { |
| this.chunk = chunk; |
| this.offset = offset; |
| } |
| } |
| |
| /** How this DhtReader has performed since creation. */ |
| public static class Statistics { |
| private final Map<ChunkKey, ChunkAccess> chunkAccess = new LinkedHashMap<ChunkKey, ChunkAccess>(); |
| |
| ChunkAccess access(ChunkKey chunkKey) { |
| ChunkAccess ca = chunkAccess.get(chunkKey); |
| if (ca == null) { |
| ca = new ChunkAccess(chunkKey); |
| chunkAccess.put(chunkKey, ca); |
| } |
| return ca; |
| } |
| |
| /** |
| * Number of sequential {@link ObjectIndexTable} lookups made by the |
| * reader. These were made without the support of batch lookups. |
| */ |
| public int cntObjectIndex_Load; |
| |
| /** Cycles detected in delta chains during OBJ_REF_DELTA reads. */ |
| public int deltaChainCycles; |
| |
| int recentChunks_Hits; |
| |
| int recentChunks_Miss; |
| |
| int deltaBaseCache_Hits; |
| |
| int deltaBaseCache_Miss; |
| |
| /** @return ratio of recent chunk hits, [0.00,1.00]. */ |
| public double getRecentChunksHitRatio() { |
| int total = recentChunks_Hits + recentChunks_Miss; |
| return ((double) recentChunks_Hits) / total; |
| } |
| |
| /** @return ratio of delta base cache hits, [0.00,1.00]. */ |
| public double getDeltaBaseCacheHitRatio() { |
| int total = deltaBaseCache_Hits + deltaBaseCache_Miss; |
| return ((double) deltaBaseCache_Hits) / total; |
| } |
| |
| /** |
| * @return collection of chunk accesses made by the application code |
| * against this reader. The collection's iterator has no |
| * relevant order. |
| */ |
| public Collection<ChunkAccess> getChunkAccess() { |
| return chunkAccess.values(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder b = new StringBuilder(); |
| b.append("DhtReader.Statistics:\n"); |
| b.append(" "); |
| if (recentChunks_Hits != 0 || recentChunks_Miss != 0) |
| ratio(b, "recentChunks", getRecentChunksHitRatio()); |
| if (deltaBaseCache_Hits != 0 || deltaBaseCache_Miss != 0) |
| ratio(b, "deltaBaseCache", getDeltaBaseCacheHitRatio()); |
| appendFields(this, b); |
| b.append("\n"); |
| for (ChunkAccess ca : getChunkAccess()) { |
| b.append(" "); |
| b.append(ca.toString()); |
| b.append("\n"); |
| } |
| return b.toString(); |
| } |
| |
| @SuppressWarnings("boxing") |
| static void ratio(StringBuilder b, String name, double value) { |
| b.append(String.format(" %s=%.2f%%", name, value * 100.0)); |
| } |
| |
| static void appendFields(Object obj, StringBuilder b) { |
| try { |
| for (Field field : obj.getClass().getDeclaredFields()) { |
| String n = field.getName(); |
| |
| if (field.getType() == Integer.TYPE |
| && (field.getModifiers() & Modifier.PUBLIC) != 0) { |
| int v = field.getInt(obj); |
| if (0 < v) |
| b.append(' ').append(n).append('=').append(v); |
| } |
| } |
| } catch (IllegalArgumentException e) { |
| throw new RuntimeException(e); |
| } catch (IllegalAccessException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** Summary describing how a chunk was accessed. */ |
| public static final class ChunkAccess { |
| /** Chunk this access block describes. */ |
| public final ChunkKey chunkKey; |
| |
| /** |
| * Number of times chunk was loaded sequentially. Incremented when |
| * the reader had to load the chunk on demand with no cache or |
| * prefetcher support. |
| */ |
| public int cntReader_Load; |
| |
| Throwable locReader_Load; |
| |
| /** |
| * Number of times the prefetcher loaded from the database. |
| * Incremented each time the prefetcher asked for the chunk from the |
| * underlying database (which might have its own distributed cache, |
| * or not). |
| */ |
| public int cntPrefetcher_Load; |
| |
| /** |
| * Number of times the prefetcher ordering was wrong. Incremented if |
| * a reader wants a chunk but the prefetcher didn't have it ready at |
| * the time of request. This indicates a bad prefetching plan as the |
| * chunk should have been listed earlier in the prefetcher's list. |
| */ |
| public int cntPrefetcher_OutOfOrder; |
| |
| /** |
| * Number of times the reader had to stall to wait for a chunk that |
| * is currently being prefetched to finish loading and become ready. |
| * This indicates the prefetcher may have fetched other chunks first |
| * (had the wrong order), or does not have a deep enough window to |
| * hide these loads from the application. |
| */ |
| public int cntPrefetcher_WaitedForLoad; |
| |
| /** |
| * Number of times the reader asked the prefetcher for the same |
| * chunk after it was already consumed from the prefetcher. This |
| * indicates the reader has walked back on itself and revisited a |
| * chunk again. |
| */ |
| public int cntPrefetcher_Revisited; |
| |
| /** |
| * Number of times the reader needed this chunk to copy an object |
| * as-is into a pack stream, but the prefetcher didn't have it |
| * ready. This correlates with {@link #cntPrefetcher_OutOfOrder} or |
| * {@link #cntPrefetcher_Revisited}. |
| */ |
| public int cntCopyObjectAsIs_PrefetchMiss; |
| |
| /** |
| * Number of times the reader tried to copy an object from this |
| * chunk, but discovered the chunk was corrupt or did not contain |
| * the object as expected. |
| */ |
| public int cntCopyObjectAsIs_InvalidChunk; |
| |
| ChunkAccess(ChunkKey key) { |
| chunkKey = key; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder b = new StringBuilder(); |
| b.append(chunkKey).append('['); |
| appendFields(this, b); |
| b.append(" ]"); |
| if (locReader_Load != null) { |
| StringWriter sw = new StringWriter(); |
| locReader_Load.printStackTrace(new PrintWriter(sw)); |
| b.append(sw); |
| } |
| return b.toString(); |
| } |
| } |
| } |
| } |