blob: 3b6694bb3d6b44805e3c4ae59e6998c311657eb3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016 Google, Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stefan Xenos (Google) - Initial implementation
*******************************************************************************/
package org.eclipse.jdt.internal.core.nd;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.internal.core.nd.db.ChunkCache;
import org.eclipse.jdt.internal.core.nd.db.Database;
import org.eclipse.jdt.internal.core.nd.db.IndexException;
/**
* Network Database for storing semantic information.
*/
public final class Nd {
private static final int CANCELLATION_CHECK_INTERVAL = 500;
private static final int BLOCKED_WRITE_LOCK_OUTPUT_INTERVAL = 30000;
private static final int LONG_WRITE_LOCK_REPORT_THRESHOLD = 1000;
private static final int LONG_READ_LOCK_WAIT_REPORT_THRESHOLD = 1000;
public static boolean sDEBUG_LOCKS= false;
public static boolean DEBUG_DUPLICATE_DELETIONS = false;
private final int currentVersion;
private final int maxVersion;
private final int minVersion;
public static int version(int major, int minor) {
return (major << 16) + minor;
}
/**
* Returns the version that shall be used when creating new databases.
*/
public int getDefaultVersion() {
return this.currentVersion;
}
public boolean isSupportedVersion(int vers) {
return vers >= this.minVersion && vers <= this.maxVersion;
}
public int getMinSupportedVersion() {
return this.minVersion;
}
public int getMaxSupportedVersion() {
return this.maxVersion;
}
public static String versionString(int version) {
final int major= version >> 16;
final int minor= version & 0xffff;
return "" + major + '.' + minor; //$NON-NLS-1$
}
// Local caches
protected Database db;
private File fPath;
private final HashMap<Object, Object> fResultCache = new HashMap<>();
private final NdNodeTypeRegistry<NdNode> fNodeTypeRegistry;
private HashMap<Long, Object> pendingDeletions = new HashMap<>();
private IReader fReader = new IReader() {
@Override
public void close() {
releaseReadLock();
}
};
/**
* This long is incremented every time a change is written to the database. Can be used to determine if the database
* has changed.
*/
private long fWriteNumber;
public Nd(File dbPath, NdNodeTypeRegistry<NdNode> nodeTypes, int minVersion, int maxVersion,
int currentVersion) throws IndexException {
this(dbPath, ChunkCache.getSharedInstance(), nodeTypes, minVersion, maxVersion, currentVersion);
}
public Nd(File dbPath, ChunkCache chunkCache, NdNodeTypeRegistry<NdNode> nodeTypes, int minVersion,
int maxVersion, int currentVersion) throws IndexException {
this.currentVersion = currentVersion;
this.maxVersion = maxVersion;
this.minVersion = minVersion;
this.fNodeTypeRegistry = nodeTypes;
loadDatabase(dbPath, chunkCache);
if (sDEBUG_LOCKS) {
this.fLockDebugging = new HashMap<>();
System.out.println("Debugging database Locks"); //$NON-NLS-1$
}
}
public File getPath() {
return this.fPath;
}
public long getWriteNumber() {
return this.fWriteNumber;
}
public void scheduleDeletion(long addressOfNodeToDelete) {
if (this.pendingDeletions.containsKey(addressOfNodeToDelete)) {
logDoubleDeletion(addressOfNodeToDelete);
return;
}
Object data = Boolean.TRUE;
if (DEBUG_DUPLICATE_DELETIONS) {
data = new RuntimeException();
}
this.pendingDeletions.put(addressOfNodeToDelete, data);
}
protected void logDoubleDeletion(long addressOfNodeToDelete) {
// Sometimes an object can be scheduled for deletion twice, if it is created and then discarded shortly
// afterward during indexing. This may indicate an inefficiency in the indexer but is not necessarily
// a bug.
// If you're debugging issues related to duplicate deletions, set DEBUG_DUPLICATE_DELETIONS to true
Package.log("Database object queued for deletion twice", new RuntimeException()); //$NON-NLS-1$
Object earlierData = this.pendingDeletions.get(addressOfNodeToDelete);
if (earlierData instanceof RuntimeException) {
RuntimeException exception = (RuntimeException) earlierData;
Package.log("Data associated with earlier deletion stack was:", exception); //$NON-NLS-1$
}
}
/**
* Synchronously processes all pending deletions
*/
public void processDeletions() {
while (!this.pendingDeletions.isEmpty()) {
long next = this.pendingDeletions.keySet().iterator().next();
deleteIfUnreferenced(next);
this.pendingDeletions.remove(next);
}
}
/**
* Returns whether this {@link Nd} can never be written to. Writable subclasses should return false.
*/
protected boolean isPermanentlyReadOnly() {
return false;
}
private void loadDatabase(File dbPath, ChunkCache cache) throws IndexException {
this.fPath= dbPath;
final boolean lockDB= this.db == null || this.lockCount != 0;
clearCaches();
this.db = new Database(this.fPath, cache, getDefaultVersion(), isPermanentlyReadOnly());
this.db.setLocked(lockDB);
if (!isSupportedVersion()) {
Package.logInfo("Index database uses the unsupported version " + this.db.getVersion() //$NON-NLS-1$
+ ". Deleting and recreating."); //$NON-NLS-1$
this.db.close();
this.fPath.delete();
this.db = new Database(this.fPath, cache, getDefaultVersion(), isPermanentlyReadOnly());
this.db.setLocked(lockDB);
}
this.fWriteNumber = this.db.getLong(Database.WRITE_NUMBER_OFFSET);
this.db.setLocked(this.lockCount != 0);
}
public Database getDB() {
return this.db;
}
// Read-write lock rules. Readers don't conflict with other readers,
// Writers conflict with readers, and everyone conflicts with writers.
private final Object mutex = new Object();
private int lockCount;
private int waitingReaders;
private long lastWriteAccess= 0;
//private long lastReadAccess= 0;
private long timeWriteLockAcquired;
private Thread writeLockOwner;
public IReader acquireReadLock() {
try {
long t = sDEBUG_LOCKS ? System.nanoTime() : 0;
synchronized (this.mutex) {
++this.waitingReaders;
try {
while (this.lockCount < 0)
this.mutex.wait();
} finally {
--this.waitingReaders;
}
++this.lockCount;
this.db.setLocked(true);
if (sDEBUG_LOCKS) {
t = (System.nanoTime() - t) / 1000000;
if (t >= LONG_READ_LOCK_WAIT_REPORT_THRESHOLD) {
System.out.println("Acquired index read lock after " + t + " ms wait."); //$NON-NLS-1$//$NON-NLS-2$
}
incReadLock(this.fLockDebugging);
}
return this.fReader;
}
} catch (InterruptedException e) {
throw new OperationCanceledException();
}
}
public void releaseReadLock() {
synchronized (this.mutex) {
assert this.lockCount > 0: "No lock to release"; //$NON-NLS-1$
if (sDEBUG_LOCKS) {
decReadLock(this.fLockDebugging);
}
//this.lastReadAccess= System.currentTimeMillis();
if (this.lockCount > 0)
--this.lockCount;
this.mutex.notifyAll();
this.db.setLocked(this.lockCount != 0);
}
// A lock release probably means that some AST is going away. The result cache has to be
// cleared since it may contain objects belonging to the AST that is going away. A failure
// to release an AST object would cause a memory leak since the whole AST would remain
// pinned to memory.
// TODO(sprigogin): It would be more efficient to replace the global result cache with
// separate caches for each AST.
//clearResultCache();
}
/**
* Acquire a write lock on this {@link Nd}. Blocks until any existing read/write locks are released.
* @throws OperationCanceledException
* @throws IllegalStateException if this {@link Nd} is not writable
*/
public void acquireWriteLock(IProgressMonitor monitor) {
try {
acquireWriteLock(0, monitor);
} catch (InterruptedException e) {
throw new OperationCanceledException();
}
}
/**
* Acquire a write lock on this {@link Nd}, giving up the specified number of read locks first. Blocks
* until any existing read/write locks are released.
* @throws InterruptedException
* @throws IllegalStateException if this {@link Nd} is not writable
*/
public void acquireWriteLock(int giveupReadLocks, IProgressMonitor monitor) throws InterruptedException {
assert !isPermanentlyReadOnly();
synchronized (this.mutex) {
if (sDEBUG_LOCKS) {
incWriteLock(giveupReadLocks);
}
if (giveupReadLocks > 0) {
// give up on read locks
assert this.lockCount >= giveupReadLocks: "Not enough locks to release"; //$NON-NLS-1$
if (this.lockCount < giveupReadLocks) {
giveupReadLocks= this.lockCount;
}
} else {
giveupReadLocks= 0;
}
// Let the readers go first
long start= sDEBUG_LOCKS ? System.currentTimeMillis() : 0;
while (this.lockCount > giveupReadLocks || this.waitingReaders > 0 || (this.lockCount < 0)) {
this.mutex.wait(CANCELLATION_CHECK_INTERVAL);
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
if (sDEBUG_LOCKS) {
start = reportBlockedWriteLock(start, giveupReadLocks);
}
}
this.lockCount= -1;
if (sDEBUG_LOCKS)
this.timeWriteLockAcquired = System.currentTimeMillis();
this.db.setExclusiveLock();
if (this.writeLockOwner != null && this.writeLockOwner != Thread.currentThread()) {
throw new IllegalStateException("We somehow managed to acquire a write lock while another thread already holds it."); //$NON-NLS-1$
}
this.writeLockOwner = Thread.currentThread();
}
}
public final void releaseWriteLock() {
releaseWriteLock(0, true);
}
@SuppressWarnings("nls")
public void releaseWriteLock(int establishReadLocks, boolean flush) {
synchronized (this.mutex) {
Thread current = Thread.currentThread();
if (current != this.writeLockOwner) {
throw new IllegalStateException("Index wasn't locked by this thread!!!");
}
this.writeLockOwner = null;
}
boolean wasInterrupted = false;
// When all locks are released we can clear the result cache.
if (establishReadLocks == 0) {
processDeletions();
this.db.putLong(Database.WRITE_NUMBER_OFFSET, ++this.fWriteNumber);
clearResultCache();
}
try {
wasInterrupted = this.db.giveUpExclusiveLock(flush) || wasInterrupted;
} catch (IndexException e) {
Package.log(e);
}
assert this.lockCount == -1;
this.lastWriteAccess= System.currentTimeMillis();
synchronized (this.mutex) {
if (sDEBUG_LOCKS) {
long timeHeld = this.lastWriteAccess - this.timeWriteLockAcquired;
if (timeHeld >= LONG_WRITE_LOCK_REPORT_THRESHOLD) {
System.out.println("Index write lock held for " + timeHeld + " ms");
}
decWriteLock(establishReadLocks);
}
if (this.lockCount < 0)
this.lockCount= establishReadLocks;
this.mutex.notifyAll();
this.db.setLocked(this.lockCount != 0);
}
if (wasInterrupted) {
throw new OperationCanceledException();
}
}
public boolean hasWaitingReaders() {
synchronized (this.mutex) {
return this.waitingReaders > 0;
}
}
public long getLastWriteAccess() {
return this.lastWriteAccess;
}
public boolean isSupportedVersion() throws IndexException {
final int version = this.db.getVersion();
return version >= this.minVersion && version <= this.maxVersion;
}
public void close() throws IndexException {
this.db.close();
clearCaches();
}
private void clearCaches() {
// fileIndex= null;
// tagIndex = null;
// indexOfDefectiveFiles= null;
// indexOfFiledWithUnresolvedIncludes= null;
// fLinkageIDCache.clear();
clearResultCache();
}
public void clearResultCache() {
synchronized (this.fResultCache) {
this.fResultCache.clear();
}
}
public Object getCachedResult(Object key) {
synchronized (this.fResultCache) {
return this.fResultCache.get(key);
}
}
public void putCachedResult(Object key, Object result) {
putCachedResult(key, result, true);
}
public Object putCachedResult(Object key, Object result, boolean replace) {
synchronized (this.fResultCache) {
Object old= this.fResultCache.put(key, result);
if (old != null && !replace) {
this.fResultCache.put(key, old);
return old;
}
return result;
}
}
public void removeCachedResult(Object key) {
synchronized (this.fResultCache) {
this.fResultCache.remove(key);
}
}
// For debugging lock issues
static class DebugLockInfo {
int fReadLocks;
int fWriteLocks;
List<StackTraceElement[]> fTraces= new ArrayList<>();
public int addTrace() {
this.fTraces.add(Thread.currentThread().getStackTrace());
return this.fTraces.size();
}
@SuppressWarnings("nls")
public void write(String threadName) {
System.out.println("Thread: '" + threadName + "': " + this.fReadLocks + " readlocks, " + this.fWriteLocks + " writelocks");
for (StackTraceElement[] trace : this.fTraces) {
System.out.println(" Stacktrace:");
for (StackTraceElement ste : trace) {
System.out.println(" " + ste);
}
}
}
public void inc(DebugLockInfo val) {
this.fReadLocks+= val.fReadLocks;
this.fWriteLocks+= val.fWriteLocks;
this.fTraces.addAll(val.fTraces);
}
}
// For debugging lock issues
private Map<Thread, DebugLockInfo> fLockDebugging;
// For debugging lock issues
private static DebugLockInfo getLockInfo(Map<Thread, DebugLockInfo> lockDebugging) {
assert sDEBUG_LOCKS;
Thread key = Thread.currentThread();
DebugLockInfo result= lockDebugging.get(key);
if (result == null) {
result= new DebugLockInfo();
lockDebugging.put(key, result);
}
return result;
}
// For debugging lock issues
static void incReadLock(Map<Thread, DebugLockInfo> lockDebugging) {
DebugLockInfo info = getLockInfo(lockDebugging);
info.fReadLocks++;
if (info.addTrace() > 10) {
outputReadLocks(lockDebugging);
}
}
// For debugging lock issues
@SuppressWarnings("nls")
static void decReadLock(Map<Thread, DebugLockInfo> lockDebugging) throws AssertionError {
DebugLockInfo info = getLockInfo(lockDebugging);
if (info.fReadLocks <= 0) {
outputReadLocks(lockDebugging);
throw new AssertionError("Superfluous releaseReadLock");
}
if (info.fWriteLocks != 0) {
outputReadLocks(lockDebugging);
throw new AssertionError("Releasing readlock while holding write lock");
}
if (--info.fReadLocks == 0) {
lockDebugging.remove(Thread.currentThread());
} else {
info.addTrace();
}
}
// For debugging lock issues
@SuppressWarnings("nls")
private void incWriteLock(int giveupReadLocks) throws AssertionError {
DebugLockInfo info = getLockInfo(this.fLockDebugging);
if (info.fReadLocks != giveupReadLocks) {
outputReadLocks(this.fLockDebugging);
throw new AssertionError("write lock with " + giveupReadLocks + " readlocks, expected " + info.fReadLocks);
}
if (info.fWriteLocks != 0)
throw new AssertionError("Duplicate write lock");
info.fWriteLocks++;
}
// For debugging lock issues
private void decWriteLock(int establishReadLocks) throws AssertionError {
DebugLockInfo info = getLockInfo(this.fLockDebugging);
if (info.fReadLocks != establishReadLocks)
throw new AssertionError("release write lock with " + establishReadLocks + " readlocks, expected " + info.fReadLocks); //$NON-NLS-1$ //$NON-NLS-2$
if (info.fWriteLocks != 1)
throw new AssertionError("Wrong release write lock"); //$NON-NLS-1$
info.fWriteLocks= 0;
if (info.fReadLocks == 0) {
this.fLockDebugging.remove(Thread.currentThread());
}
}
// For debugging lock issues
@SuppressWarnings("nls")
private long reportBlockedWriteLock(long start, int giveupReadLocks) {
long now= System.currentTimeMillis();
if (now >= start + BLOCKED_WRITE_LOCK_OUTPUT_INTERVAL) {
System.out.println();
System.out.println("Blocked writeLock");
System.out.println(" lockcount= " + this.lockCount + ", giveupReadLocks=" + giveupReadLocks + ", waitingReaders=" + this.waitingReaders);
outputReadLocks(this.fLockDebugging);
start= now;
}
return start;
}
// For debugging lock issues
@SuppressWarnings("nls")
private static void outputReadLocks(Map<Thread, DebugLockInfo> lockDebugging) {
System.out.println("--------------------- Lock Debugging -------------------------");
for (Thread th: lockDebugging.keySet()) {
DebugLockInfo info = lockDebugging.get(th);
info.write(th.getName());
}
System.out.println("---------------------------------------------------------------");
}
// For debugging lock issues
public void adjustThreadForReadLock(Map<Thread, DebugLockInfo> lockDebugging) {
for (Thread th : lockDebugging.keySet()) {
DebugLockInfo val= lockDebugging.get(th);
if (val.fReadLocks > 0) {
DebugLockInfo myval= this.fLockDebugging.get(th);
if (myval == null) {
myval= new DebugLockInfo();
this.fLockDebugging.put(th, myval);
}
myval.inc(val);
for (int i = 0; i < val.fReadLocks; i++) {
decReadLock(this.fLockDebugging);
}
}
}
}
public NdNode getNode(long address, short nodeType) throws IndexException {
return this.fNodeTypeRegistry.createNode(this, address, nodeType);
}
public <T extends NdNode> ITypeFactory<T> getTypeFactory(short nodeType) {
return this.fNodeTypeRegistry.getTypeFactory(nodeType);
}
/**
* Returns the type ID for the given class
*/
public short getNodeType(Class<? extends NdNode> toQuery) {
return this.fNodeTypeRegistry.getTypeForClass(toQuery);
}
private void deleteIfUnreferenced(long address) {
if (address == 0) {
return;
}
short nodeType = NdNode.NODE_TYPE.get(this, address);
// Look up the type
ITypeFactory<? extends NdNode> factory1 = getTypeFactory(nodeType);
if (factory1.isReadyForDeletion(this, address)) {
// Call its destructor
factory1.destruct(this, address);
// Free up its memory
getDB().free(address, (short)(Database.POOL_FIRST_NODE_TYPE + nodeType));
}
}
public void delete(long address) {
if (address == 0) {
return;
}
short nodeType = NdNode.NODE_TYPE.get(this, address);
// Look up the type
ITypeFactory<? extends NdNode> factory1 = getTypeFactory(nodeType);
// Call its destructor
factory1.destruct(this, address);
// Free up its memory
getDB().free(address, (short)(Database.POOL_FIRST_NODE_TYPE + nodeType));
// If this node was in the list of pending deletions, remove it since it's now been deleted
if (this.pendingDeletions.containsKey(address)) {
logDoubleDeletion(address);
this.pendingDeletions.remove(address);
}
}
public NdNodeTypeRegistry<NdNode> getTypeRegistry() {
return this.fNodeTypeRegistry;
}
public void clear(IProgressMonitor monitor) {
getDB().clear(getDefaultVersion());
}
public boolean isValidAddress(long address) {
return address > 0 && address < getDB().getChunkCount() * Database.CHUNK_SIZE;
}
}