package com.cosylab.acs.maci.manager; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * A Lock that internally delegates to a {@link ReentrantLock} and profiles the time * it takes a thread to acquire the lock, and the time for which the lock is held. * When a thread calls {@link #unlock()} then these times are printed, together with the code location * which used the lock. * <p> * This lock can be used as an alternative to "synchronized" blocks, when we want to detect lock contention. * It may also be used as an alternative to some other {@link Lock}, although at the moment only methods {@link Lock#lock()} * and {@link Lock#unlock()} are implemented. For the other Lock methods we throw {@link UnsupportedOperationException}, * but it should be easy enough to also implement them when needed. * <p> * This class is added to jmanager for work on http://jira.alma.cl/browse/COMP-6488. * Later it should be moved to module jacsutil. * * @author hsommer * @since ACS 10.2 */ public class ProfilingReentrantLock implements Lock { private final ReentrantLock delegate; private final String lockName; public static final String PROFILING_ENABLED_PROPERTYNAME = "acs.enableReentrantLockProfiling"; public static final boolean isProfilingEnabled = Boolean.getBoolean(PROFILING_ENABLED_PROPERTYNAME); public ProfilingReentrantLock(String lockName) { this.lockName = lockName; this.delegate = new ReentrantLock(); } /** * Factory method that encapsulates the checking of property {@link #PROFILING_ENABLED_PROPERTYNAME} * and the corresponding creation of either a ProfilingReentrantLock or a ReentrantLock. * <p> * We return only the base type "Lock" to keep the implementation of ProfilingReentrantLock simple for the time being. * Some methods of ReentrantLock are final, so that delegation anyway is easier than subtyping. * If we really need the additional methods, we could refactor ProfilingReentrantLock to inherit from ReentrantLock * and change the return type to ReentrantLock. * <p> * @TODO: Perhaps also pass a Logger and the "fair" boolean. * @param lockName Only used for the profiling variant, for log output. */ public static Lock createReentrantLock(String lockName) { if (isProfilingEnabled) { return new ProfilingReentrantLock(lockName); } else { return new ReentrantLock(); } } private static class LockInfo { public LockInfo(String codeId, long timeAttempted, int queueLength) { this.codeId = codeId; this.timeAttempted = timeAttempted; this.queueLength = queueLength; } final String codeId; final long timeAttempted; final int queueLength; long timeLocked; long timeUnlocked; } /** * key = thread id */ private Map<Long, LockInfo> lockInfoMap = new HashMap<Long, LockInfo>(); @Override public void lock() { if (!delegate.isHeldByCurrentThread()) { String codeId = createCodeId(1); long t0 = System.currentTimeMillis(); int queueLength = delegate.getQueueLength(); LockInfo lockInfo = new LockInfo(codeId, t0, queueLength); lockInfoMap.put(Thread.currentThread().getId(), lockInfo); delegate.lock(); lockInfo.timeLocked = System.currentTimeMillis(); } else { // no profiling for nested locking, just lock delegate.lock(); } } @Override public void lockInterruptibly() throws InterruptedException { throw new UnsupportedOperationException("lockInterruptibly not yet supported"); // delegate.lockInterruptibly(); } @Override public boolean tryLock() { throw new UnsupportedOperationException("tryLock not yet supported"); // return delegate.tryLock(); } @Override public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException("tryLock not yet supported"); // return delegate.tryLock(timeout, unit); } @Override public void unlock() { delegate.unlock(); if (!delegate.isHeldByCurrentThread()) { LockInfo lockInfo = lockInfoMap.remove(Thread.currentThread().getId()); lockInfo.timeUnlocked = System.currentTimeMillis(); printProfilingMessage(lockInfo, Thread.currentThread().getName()); } else { // no profiling for nested unlocking when we still hold the lock } } @Override public Condition newCondition() { return delegate.newCondition(); } /** * @return The file name and line of file from where this method gets called, skipping an extra <code>skipLevels</code> from the call stack. * <code>null</code> if no call stack can be retrieved. */ String createCodeId(int skipLevels) { String ret = null; StackTraceElement[] stackTraceElems = (new Throwable()).getStackTrace(); if (stackTraceElems.length >= skipLevels + 1) { ret = stackTraceElems[skipLevels + 1].getFileName() + "/" + stackTraceElems[skipLevels + 1].getLineNumber(); } else { ret = "<unknown>"; } return ret; } private void printProfilingMessage(LockInfo lockInfo, String threadName) { System.out.println("Profiling for lock '" + lockName + "' used in code='" + lockInfo.codeId + "', thread='" + threadName + "': queueLength=" + lockInfo.queueLength + ", acquiredMillis=" + (lockInfo.timeLocked - lockInfo.timeAttempted) + ", heldMillis=" + (lockInfo.timeUnlocked - lockInfo.timeLocked)); } }