Logo Search packages:      
Sourcecode: cacao-oj6 version File versions

TestResultCache.java

/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import com.sun.javatest.util.Debug;
import com.sun.javatest.util.Fifo;
import com.sun.javatest.util.I18NResourceBundle;

/**
 * Class which maintains a cache of the results currently existing inside a
 * work directory.  It is designed to allow the harness to get general
 * information (name, status) about tests without having to open all the
 * result files.
 */
00047 public class TestResultCache {
    /**
     * Interface which allows an external class to monitor the cache's
     * activities.
     */
00052     public static interface Observer {
      /**
       * Called when tests have been read from the cache file.
       * @param tests the tests that have been read
       */
      void update(Map tests);

      /**
       * Called periodically while waiting to access the cache.
       * @param timeSoFar the time so far that a client has been waiting to
       * access the cache
       */
      void waitingForLock(long timeSoFar);

      /**
       * Called when the timed out waiting for access to the cache.
       */
      void timeoutWaitingForLock();

      /**
       * Called when the cache has been locked.
       */
      void acquiredLock();

      /**
       * Called when the lock on the cache has been released.
       */
      void releasedLock();

      /**
       * Called when starting to (re)build the cache.
       * @param reset currently, always true
       */
      void buildingCache(boolean reset);

      /**
       * Called when a test has been found to put in the cache.
       * @param tr the test that is being put in the cache
       */
      void buildingCache(TestResult tr);

      /**
       * Called when the cache has been (re)built.
       */
      void builtCache();

      /**
       * Called when a serious error has occurred and the cache is unable to continue.
       * @param t an object identifying the error that occurred
       */
      void error(Throwable t);
    };

    /**
     * Primary constructor for a cache.
     * @param workDir the work directory to attach to
     * @param observer the observer to notify of cache events
     * @throws IOException if an error occurs reading the cache file
     */
00111     public TestResultCache(WorkDirectory workDir, Observer observer)
                      throws IOException {
      this.workDir = workDir;
      this.observer = observer;

      weakWorkDir = new WeakReference(workDir);
      weakObserver = new WeakReference(observer);

      cacheFile = workDir.getSystemFile(V2_FILENAME);
      lockFile = workDir.getSystemFile(V2_LOCKNAME);

      File old = workDir.getSystemFile(V1_FILENAME);
      if (old.exists()) {
          workDir.log(i18n, "trc.rmCachev1", old.getAbsolutePath());
          old.delete();
      }

      old = workDir.getSystemFile(V1_LOCKNAME);
      if (old.exists()) {
          workDir.log(i18n, "trc.rmLockv1", old.getAbsolutePath());
          old.delete();
      }

      raf = new RandomAccessFile(cacheFile, "rw");

      worker = new Thread() {
            public void run() {
                doWorkUntilDone();
            }
          };
      worker.setName("TestResultCache.worker" + (workerNumber++) + "[" + workDir.getRoot() + "]");
      worker.setDaemon(true); // allows thread to run during shutdown

      // ensure priority is lower than that of the calling thread, but don't 
      // want priority too low because we want cache updated in a timely fashion
      int prio = Math.max(0, Math.min(Thread.currentThread().getPriority() - 1, 3));
      worker.setPriority(prio);

      worker.start();

      shutdownHandler = new Thread() {
            public void run() {
                shutdown();
                }
          };
      Runtime.getRuntime().addShutdownHook(shutdownHandler);
    }

    /**
     * Insert a test result into the cache. 
     * The cache file will be updated asynchronously.
     * @param tr the test to be inserted.
     **/
00164     synchronized void insert(TestResult tr) {
      if (DEBUG_TESTS)
          Debug.println("TRC.insert " + tr.getWorkRelativePath() + " " + tr.getStatus());

      reviveWeakReferences();
      testsToWrite.insert(tr);
      notifyAll();
    }

    /**
     * Request an update via the observer of all the tests in the cache.
     */
00176     synchronized void requestFullUpdate() {
      if (!shutdownRequested) {
          reviveWeakReferences();
          fullUpdateRequested = true;
          notifyAll();
      }
    }

    /**
     * Check if a call to compress the cache is advised.
     */
00187     synchronized boolean needsCompress() {
      return compressNeeded; 
    }

    /**
     * Compress the cache file, eliminating any obsolete entries.
     * The cache file will be updated asynchronously.
     */
00195     synchronized void compress() {
      if (!shutdownRequested) {
          reviveWeakReferences();
          compressRequested = true;
          notifyAll();
      }
    }

    /**
     * Expedite any outstanding updates to the cache file.
     * The cache file will be updated asynchronously.
     */
00207     synchronized void flush() {
      reviveWeakReferences();
      flushRequested = true;
      notifyAll();
    }

    //-------------------------------------------------------------------------------------
    //
    // the main worker code

    private static final int DEFAULT_COMPRESS_PERCENT_LEVEL = 40;

    private void doWorkUntilDone() {
      int compressPercentLevel = 
          Integer.getInteger("javatest.trc.cacheThreshold",
                         DEFAULT_COMPRESS_PERCENT_LEVEL).intValue();
      compressPercentLevel = Math.max(1, Math.min(compressPercentLevel, 100));

      boolean haveWork = true;  // first time in, we need to read/build the cache
      fullUpdateRequested = true; // first time in, update client when initialized

      try {
          while (haveWork) {
            doWork();

                synchronized (this) {
                if (flushRequested && testsToWrite.size() == 0)
                  flushRequested = false;

                long timeLastWork = System.currentTimeMillis();
                haveWork = isWorkAvailable(timeLastWork);
                while (!haveWork && !shutdownRequested) {
                  workDir = null;
                  observer = null;

                  if (DEBUG_SYNC)
                      Debug.println("TRC.worker waiting");

                  wait(MIN_TEST_READ_INTERVAL);
                  haveWork = isWorkAvailable(timeLastWork);

                  if (DEBUG_SYNC)
                      Debug.println("TRC.worker awake haveWork=" + haveWork);
                }
                
                // wake up the workDir and observer (if still available)
                reviveWeakReferences();

                // if workDir and/or observer are null, then the WD and TRT
                // that opened this cache have been GC-ed. This means that this 
                // thread has woken up for an external reason, such as noticing
                // that the cache file size has changed.  Since the client has
                // gone away, there is nothing left to do but return.
                if (workDir == null || observer == null)
                  return;
                
                // re-evaluate compressNeeded now, while still synchronized
                // so that totalEntryCount and uniqueInitialEntry do not have
                // to be synchronized inside doWork()
                if (totalEntryCount == 0)
                  compressNeeded = false;
                else {
                  // when we read/wrote the beginning of the cache file, there was a set of 
                  // unique entries. Everything after that is essentially unknown.
                  // If that latter part gets disproportionately big, set compressNeeded.
                  int uncompressedEntryCount = totalEntryCount - uniqueInitialEntryCount;
                  compressNeeded = (uncompressedEntryCount * 100 / totalEntryCount > compressPercentLevel);
                }
            }
          }
      }
      catch (IOException e) {
          // A serious error has occurred that cannot be recovered from;
          // report it, and give up
          observer.error(e);
      }
      catch (InterruptedException e) {
          // should never happen, but if it does, we were idle anyway
          // so just return, and exit the worker thread
      }
      finally {
          try {
            raf.close();
          }
          catch (IOException e) {
            // ignore
          }

          synchronized (this) {
            worker = null;
            notifyAll();
          }
      }
    }

    private void doWork() throws IOException {
      Map tests = null;
      boolean rebuildCache = false;
      
      getLock();
      
      try {
          // if cache file exists and has content, read it all or read updates 
          // as appropriate; if any errors occur, zap the file, so it will be rebuilt
          try {
            if (raf.length() > 0) {
                tests = readCache();

                    // special case
                    if (tests == null)
                  rebuildCache = true;
            }
            else
                rebuildCache = true;
          }
          catch (Throwable e) {
            // cache appears to be corrupt; empty it and rebuild it
            if (DEBUG_BASIC)
                Debug.println("TRC.corrupt " + e);
            workDir.log(i18n, "trc.reloadFault", e);
            rebuildCache = true;
            raf.setLength(0);
          }

          // if we're rebuilding the cache and the VM has started to shut down,
          // quit rebuilding the cache and give up, as quickly as possible,
          // leaving the next client to rebuild the cache instead
          if (rebuildCache && shutdownRequested) {
            testsToWrite.flush();
            raf.setLength(0);
            return;
          }
          
          // if cache is empty, rebuild it from .jtr files; 
          // note that a valid cache with no tests has length 4
          if (rebuildCache) {
            observer.buildingCache(rebuildCache);
            tests = readJTRFiles();
            observer.builtCache();
          }

          if (rebuildCache || compressRequested || raf.length() == 0) {
            writeCache(tests);
            // it is safe to clear the compressRequested flag because the client
            // can only set it to true (not false, nor read it)
            compressRequested = false;
          }
          
          // write any outstanding tests
          updateCache(tests);
      }
      finally {
          // if we're exiting abnormally, leave the next user to clean up 
          // if the file is corrupt; for now, it is better to unlock the
          // file to say we're not using it any more
          releaseLock();
      }
      
      if (updateNeeded) {
          if (!shutdownRequested) 
            observer.update(tests);
          // it is safe to clear the fullUpdateRequested flag because the client
          // can only set it to true (not false, nor read it)
          fullUpdateRequested = false;
      }
    }

    private static final int MIN_TEST_READ_INTERVAL = 10000; // 10 seconds
    private static final int MIN_TEST_WRITE_INTERVAL = 10000; // 10 seconds

    private boolean isWorkAvailable(long timeLastWork) {    
      if (compressRequested || flushRequested   || fullUpdateRequested) {
          if (DEBUG_CHECK_WORK)
            Debug.println("TRC.haveWork (request" 
                        + (compressRequested ? ":compress" : "")
                        + (flushRequested ? ":flush" : "")
                        + (fullUpdateRequested ? ":update" : "") + ")");
          return true;
      }

      long now = System.currentTimeMillis();

      if (now - timeLastWork >= MIN_TEST_WRITE_INTERVAL && testsToWrite.size() > 0) {
          if (DEBUG_CHECK_WORK)
            Debug.println("TRC.haveWork (" + testsToWrite.size() + " tests)");
          return true;
      }

      try {
          if (now - timeLastWork >= MIN_TEST_READ_INTERVAL && raf.length() != lastFileSize) {
            if (DEBUG_CHECK_WORK)
                Debug.println("TRC.haveWork (file size changed: " + raf.length() + ")");
            return true;
          }
      }
      catch (IOException e) {
          // if an error occurred, we ought to let the worker thread go investigate
          if (DEBUG_CHECK_WORK)
            Debug.println("TRC.haveWork (" + e.getMessage() + ")");
          return true;
      }

      return false;
    }

    //-------------------------------------------------------------------------------------
    //
    // shutdown handler code

    private static final int MAX_SHUTDOWN_TIME = 30000; // 30 seconds

    synchronized void shutdown() {
      if (DEBUG_BASIC)
          Debug.println("TRC.worker shutdown, " + testsToWrite.size() + " tests to flush");

      // shutdownRequested is initially false, and gets set true here;
      // once set true, it stays true and never goes false again, which 
      // reduces the need for synchronized access
      shutdownRequested = true;
      if (testsToWrite.size() > 0)
          flushRequested = true;
      notifyAll();

      long now = System.currentTimeMillis();
      long end = now + MAX_SHUTDOWN_TIME;
      try {
          while (worker != null & now < end) {
            if (DEBUG_SYNC)
                Debug.println("TRC.shutdown waiting for worker to exit");

            wait(end - now);
            now = System.currentTimeMillis();
          }

          if (DEBUG_SYNC)
            Debug.println("TRC.shutdown done");
      }
      catch (InterruptedException e) {
          // ignore
      }

        // important to avoid memory leak !!!
        try {
            if (shutdownHandler != null) {
                Runtime.getRuntime().removeShutdownHook(shutdownHandler);
            }
        }
        catch(IllegalStateException ex) {
            // it's ok if shutdown is in process now
        }
    }

    //-------------------------------------------------------------------------------------
    // 
    // Read a set of tests from the *.jtr files in the work directory

    private Map readJTRFiles() {
      Map tests = new TreeMap();
      readJTRFiles(workDir.getRoot(), tests);
      return tests;
    }

    private void readJTRFiles(File dir, Map tests) {
      File[] entries = dir.listFiles();
      if (entries != null) {
          // monitor shutdownRequested and give up if set true;
          // no specific notification is passed back in this case;
          // it is assumed the caller will also check shutdownRequested
          // and act appropriately
          for (int i = 0; i < entries.length && !shutdownRequested; i++) {
            File f = entries[i];
            if (f.isDirectory())
                readJTRFiles(f, tests);
            else if (TestResult.isResultFile(f)) {
                try {
                  TestResult tr = new TestResult(f);
                  tests.put(tr.getWorkRelativePath(), tr);
                }
                catch (TestResult.ResultFileNotFoundFault e) {
                  // hmm, should not happen, since we just read the directory
                  workDir.log(i18n, "trc.lostjtr", f);
                } 
                catch (TestResult.ReloadFault e) {
                  // delete this jtr and continue
                  // should we inform TRT? perhaps via observer
                  workDir.log(i18n, "trc.badjtr", f);
                  f.delete();
                } 
            }
            entries[i] = null;  // just to help GC
          }
      }
    }

    private TestResult reload(Map tests, TestResult tr) {
      File jtr = workDir.getFile(tr.getWorkRelativePath());
      try {
          return new TestResult(jtr);
      }
      catch (TestResult.ResultFileNotFoundFault e) {
          // test is presumably not run or has been purged
          // this notRun status will persist in cache until cache rebuilt
          String name = tr.getTestName();
          tr = new TestResult(name, workDir, Status.notRun(""));
          tests.put(name, tr); // in case fullUpdateRequested
          return tr;
      }
      catch (TestResult.ReloadFault e) {
                        // bad .jtr, delete it
          workDir.log(i18n, "trc.badjtr", jtr);
          jtr.delete();
          // this notRun status will persist in cache until cache rebuilt
          String name = tr.getTestName();
          tr = new TestResult(name, workDir, Status.notRun("previous results corrupted"));
          tests.put(name, tr); // in case fullUpdateRequested
          return tr;
      }
    }

    //-------------------------------------------------------------------------------------
    // 
    // Read the cache

    private Map readCache() 
      throws IOException, IllegalArgumentException
    {
      if (DEBUG_WORK)
          Debug.println("TRC.readCache");

      raf.seek(0);
      int fileSerial = raf.readInt();

      if (DEBUG_WORK)
          Debug.println("TRC.readCache serial=" + fileSerial);

      if (lastFileSize == -1 || fileSerial != lastSerial 
          || fullUpdateRequested || compressRequested) {
          updateNeeded = (fullUpdateRequested 
                      || fileSerial != lastSerial 
                      || raf.length() > lastFileSize);
          // read full cache
          lastSerial = fileSerial;
          totalEntryCount = 0;
          Map tests = readCacheEntries();
          uniqueInitialEntryCount = tests.size();

          if (DEBUG_WORK) 
            Debug.println("TRC.readCache read all (" + tests.size() + " tests, " + uniqueInitialEntryCount + " unique)");

          return tests;
      }
      else if (raf.length() > lastFileSize) {
          // just read updates from file
          raf.seek(lastFileSize);
          Map tests = readCacheEntries();

          if (DEBUG_WORK)
            Debug.println("TRC.readCache read update (" + tests.size() + " tests)");

          updateNeeded = true;
          return tests;
      }
      else {
          // no updates available
          updateNeeded = false;
          return null;
      }
    }

    private Map readCacheEntries() 
      throws IOException, IllegalArgumentException 
    {
      Map tests = new TreeMap();
      while (raf.getFilePointer() < raf.length()) {
          String name = raf.readUTF();
          int status = raf.readInt();
          String reason = raf.readUTF();
          long endTime = raf.readLong();
          TestResult tr = new TestResult(name, workDir, new Status(status, reason), endTime);
            File f = tr.getFile();
            if (!f.exists()) {
                tr.resetFile();
            }
          tests.put(tr.getWorkRelativePath(), tr);
          totalEntryCount++; // count all entries, including duplicates
      }
      lastFileSize = raf.length();
      return tests;
    }

    //-------------------------------------------------------------------------------------
    // 
    // Write the cache

    private void writeCache(Map tests) throws IOException {
      if (tests == null)
          throw new IllegalStateException();

      // merge any tests in testsToWrite
      // testsToWrite is a thread-safe fifo, so it is safe to keep reading
      // it till its empty, even though some tests may even have been added
      // after the worker woke up
      TestResult tr;
      while ((tr = (TestResult) (testsToWrite.remove())) != null) {
          // check if test is in the set we've just read
          String name = tr.getTestName();
          TestResult tr2 = (TestResult) (tests.get(name));
          // if the cache file contains a conflicting entry,
          // reload the test from the .jtr file; otherwise, add it to the cache
          if (tr2 != null && !tr2.getStatus().equals(tr.getStatus()))
            reload(tests, tr);
          else
            tests.put(tr.getWorkRelativePath(), tr);
      }
      
      // write cache
      raf.seek(0);
      long now = System.currentTimeMillis();
      lastSerial = (int) ((now >> 16) + (now & 0xffff));
      raf.writeInt(lastSerial);

      for (Iterator iter = tests.values().iterator(); iter.hasNext(); ) {
          tr = (TestResult) (iter.next());
          writeCacheEntry(tr);
      } 

      if (DEBUG_WORK)
          Debug.println("TRC.writeCache write all (" + tests.size() + " tests)");

      lastFileSize = raf.length();
      uniqueInitialEntryCount = totalEntryCount = tests.size();
    }

    private void updateCache(Map tests) throws IOException {
      // testsToWrite is a thread-safe fifo, so it is safe to keep reading
      // it till its empty, even though some tests may even have been added
      // after the worker woke up
      int debugCount = 0;
      raf.seek(lastFileSize);
      TestResult tr;
      while ((tr = (TestResult) (testsToWrite.remove())) != null) {
          if (tests != null) {
            // check if test is in the set we've just read
            String name = tr.getTestName();
            TestResult tr2 = (TestResult) (tests.get(name));
            if (tr2 != null) {
                // cache also contains an entry for this test: 
                // reload from .jtr file in case of conflict
                if (!tr2.getStatus().equals(tr.getStatus()))
                  tr = reload(tests, tr);
            }
          }
          writeCacheEntry(tr);
          debugCount++;
      }
      if (DEBUG_WORK && debugCount > 0)
          Debug.println("TRC.writeCache write update (" + debugCount + " tests)");
      lastFileSize = raf.length();
    }

    private void writeCacheEntry(TestResult tr) throws IOException {
      String name = tr.getTestName();
      Status status = tr.getStatus();
      raf.writeUTF(name);
      raf.writeInt(status.getType());
      raf.writeUTF(status.getReason());
      raf.writeLong(tr.getEndTime());
      totalEntryCount++;
    }

    //-------------------------------------------------------------------------------------
    //
    // lock acquisition and release

    // lock acquisition and notification parameters
    private static final long INITIAL_LOCK_NOTIFY_TIME = 20000; // 20 seconds
    private static final long LOCK_NOTIFY_INTERVAL = 60000;     // 60 second
    private static final long MAX_LOCK_WAIT_TIME =
      Integer.getInteger("javatest.trc.timeout", 5).intValue() * 60000;
                                                                // default 5 minutes

    // retry parameters
    private static final int INITIAL_RETRY_DELAY_TIME = 500;    // 0.5 second
    private static final int MAX_RETRY_DELAY_TIME = 10000;      // 10 seconds

    private void getLock() throws IOException { 
      long start = System.currentTimeMillis();
      int retryDelay = INITIAL_RETRY_DELAY_TIME;
      long lastNotified = 0;
      
      // for JDK 1.4, should consider using raf.getChannel().tryLock()
      while (!lockFile.createNewFile()) {
          long now = System.currentTimeMillis();
          long timeSinceStart = now - start;
          if (timeSinceStart < INITIAL_LOCK_NOTIFY_TIME) {
            // no need to worry, yet
            continue;
          }
          else if (timeSinceStart < MAX_LOCK_WAIT_TIME) {
            // slowly getting nervous: periodically notify observer
            long timeSinceLastNotified = now - lastNotified;
            if (lastNotified == 0
                || timeSinceLastNotified > LOCK_NOTIFY_INTERVAL) {
                observer.waitingForLock(timeSinceStart);
                lastNotified = System.currentTimeMillis();
            }
            
            // slowly increase delay while we are waiting
            if (retryDelay < MAX_RETRY_DELAY_TIME)
                retryDelay = Math.min(2 * retryDelay, MAX_RETRY_DELAY_TIME);
          }
          else {
            // full fledge panic: trash the lock and rebuild the cache
            observer.timeoutWaitingForLock();
            workDir.log(i18n, "trc.lockTimeout");
            
            try {
                if (raf != null)
                  raf.close();
            }
            catch (IOException e) {
                // ignore
            }
            
            cacheFile.delete();
            lockFile.delete();
            
            raf = new RandomAccessFile(cacheFile, "rw");
            
            // leave caller to repopulate cache
          }
          
          // sleep for a bit before retrying to avoid busy waiting
          try {
            Thread.currentThread().sleep(retryDelay); 
          }
          catch (InterruptedException e) {
            // ignore
          }
          
      }

      observer.acquiredLock();
    }

    private void releaseLock() {
      // for JDK 1.4, should consider releasing the FileLock obtained from 
      // raf.getChannel().tryLock()
      lockFile.delete();
      observer.releasedLock();
    }

    //-------------------------------------------------------------------------------------

    // When the work thread sleeps, it zaps the main references to observer and workDir
    // to the possibility that the WD and TRT might be GCed, and just retains weak references.
    // These weak references get revived when the worker thread has work to do.
    private void reviveWeakReferences() {
      if (workDir == null) 
          workDir = (WorkDirectory) weakWorkDir.get();
      
      if (observer == null)
          observer = (Observer) weakObserver.get();
    }
    
    //-------------------------------------------------------------------------------------

    // general instance data
    private Observer observer;     
    private WeakReference weakObserver;
    private WorkDirectory workDir; 
    private WeakReference weakWorkDir;
    private File cacheFile;
    private File lockFile;
    private Thread worker;
    private Thread shutdownHandler;

    // worker thread data
    private RandomAccessFile raf;
    private int uniqueInitialEntryCount;
    private int totalEntryCount;
    private int lastSerial;
    private long lastFileSize;
    private boolean updateNeeded;

    // synchronized data
    private boolean fullUpdateRequested;
    private boolean compressNeeded;
    private boolean compressRequested;
    private boolean flushRequested;
    private boolean shutdownRequested;
    private Fifo testsToWrite = new Fifo();

    private static final String V1_FILENAME = "ResultCache.jtw";
    private static final String V1_LOCKNAME = V1_FILENAME + ".lck";
    private static final String V2_FILENAME = "ResultCache2.jtw";
    private static final String V2_LOCKNAME = V2_FILENAME + ".lck";

    // other
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestResultCache.class);
    private static int workerNumber; // for naming threads

    private static int debug = Debug.getInt(TestResultCache.class);
    private static final boolean DEBUG_BASIC =      (debug >= 1);  // basic messages and stack trace
    private static final boolean DEBUG_TESTS =      (debug >= 2);  // details about tests
    private static final boolean DEBUG_WORK  =      (debug >= 3);  // details about work done  
    private static final boolean DEBUG_CHECK_WORK = (debug >= 4);  // details when checking for work
    private static final boolean DEBUG_SYNC =       (debug >= 5);  // details about thread syncs
}

Generated by  Doxygen 1.6.0   Back to index