1 /*
   2  * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.jvmstat.perfdata.monitor.v2_0;
  27 
  28 import sun.jvmstat.monitor.*;
  29 import sun.jvmstat.perfdata.monitor.*;
  30 import java.util.*;
  31 import java.util.regex.*;
  32 import java.nio.*;
  33 
  34 /**
  35  * The concrete implementation of version 2.0 of the HotSpot PerfData
  36  * Instrumentation buffer. This class is responsible for parsing the
  37  * instrumentation memory and constructing the necessary objects to
  38  * represent and access the instrumentation objects contained in the
  39  * memory buffer.
  40  * <p>
  41  * The structure of the 2.0 entry is defined in struct PerfDataEnry
  42  * as decsribed in perfMemory.hpp. This structure looks like:
  43  * <pre>
  44  * typedef struct {
  45  *   jint entry_length;         // entry length in bytes
  46  *   jint name_offset;          // offset to entry name, relative to start
  47  *                              // of entry
  48  *   jint vector_length;        // length of the vector. If 0, then scalar.
  49  *   jbyte data_type;           // JNI field descriptor type
  50  *   jbyte flags;               // miscellaneous attribute flags
  51  *                              // 0x01 - supported
  52  *   jbyte data_units;          // unit of measure attribute
  53  *   jbyte data_variability;    // variability attribute
  54  *   jbyte data_offset;         // offset to data item, relative to start
  55  *                              // of entry.
  56  * } PerfDataEntry;
  57  * </pre>
  58  *
  59  * @author Brian Doherty
  60  * @since 1.5
  61  * @see AbstractPerfDataBuffer
  62  */
  63 public class PerfDataBuffer extends PerfDataBufferImpl {
  64 
  65     private static final boolean DEBUG = false;
  66     private static final int syncWaitMs =
  67             Integer.getInteger("sun.jvmstat.perdata.syncWaitMs", 5000);
  68     private static final ArrayList EMPTY_LIST = new ArrayList(0);
  69 
  70     /*
  71      * These are primarily for documentary purposes and the match up
  72      * with the PerfDataEntry structure in perfMemory.hpp. They are
  73      * generally unused in this code, but they are kept consistent with
  74      * the data structure just in case some unforseen need arrises.
  75      */
  76     private final static int PERFDATA_ENTRYLENGTH_OFFSET=0;
  77     private final static int PERFDATA_ENTRYLENGTH_SIZE=4;   // sizeof(int)
  78     private final static int PERFDATA_NAMEOFFSET_OFFSET=4;
  79     private final static int PERFDATA_NAMEOFFSET_SIZE=4;    // sizeof(int)
  80     private final static int PERFDATA_VECTORLENGTH_OFFSET=8;
  81     private final static int PERFDATA_VECTORLENGTH_SIZE=4;  // sizeof(int)
  82     private final static int PERFDATA_DATATYPE_OFFSET=12;
  83     private final static int PERFDATA_DATATYPE_SIZE=1;      // sizeof(byte)
  84     private final static int PERFDATA_FLAGS_OFFSET=13;
  85     private final static int PERFDATA_FLAGS_SIZE=1;       // sizeof(byte)
  86     private final static int PERFDATA_DATAUNITS_OFFSET=14;
  87     private final static int PERFDATA_DATAUNITS_SIZE=1;     // sizeof(byte)
  88     private final static int PERFDATA_DATAVAR_OFFSET=15;
  89     private final static int PERFDATA_DATAVAR_SIZE=1;       // sizeof(byte)
  90     private final static int PERFDATA_DATAOFFSET_OFFSET=16;
  91     private final static int PERFDATA_DATAOFFSET_SIZE=4;    // sizeof(int)
  92 
  93     PerfDataBufferPrologue prologue;
  94     int nextEntry;
  95     long lastNumEntries;
  96     IntegerMonitor overflow;
  97     ArrayList<Monitor> insertedMonitors;
  98 
  99     /**
 100      * Construct a PerfDataBuffer instance.
 101      * <p>
 102      * This class is dynamically loaded by
 103      * {@link AbstractPerfDataBuffer#createPerfDataBuffer}, and this
 104      * constructor is called to instantiate the instance.
 105      *
 106      * @param buffer the buffer containing the instrumentation data
 107      * @param lvmid the Local Java Virtual Machine Identifier for this
 108      *              instrumentation buffer.
 109      */
 110     public PerfDataBuffer(ByteBuffer buffer, int lvmid)
 111            throws MonitorException {
 112         super(buffer, lvmid);
 113         prologue = new PerfDataBufferPrologue(buffer);
 114         this.buffer.order(prologue.getByteOrder());
 115     }
 116 
 117     /**
 118      * {@inheritDoc}
 119      */
 120     protected void buildMonitorMap(Map<String, Monitor>  map) throws MonitorException {
 121         assert Thread.holdsLock(this);
 122 
 123         // start at the beginning of the buffer
 124         buffer.rewind();
 125 
 126         // create pseudo monitors
 127         buildPseudoMonitors(map);
 128 
 129         // wait for the target JVM to indicate that it's intrumentation
 130         // buffer is safely accessible
 131         synchWithTarget();
 132 
 133         // parse the currently defined entries starting at the first entry.
 134         nextEntry = prologue.getEntryOffset();
 135 
 136         // record the number of entries before parsing the structure
 137         int numEntries = prologue.getNumEntries();
 138 
 139         // start parsing
 140         Monitor monitor = getNextMonitorEntry();
 141         while (monitor != null) {
 142             map.put(monitor.getName(), monitor);
 143             monitor = getNextMonitorEntry();
 144         }
 145 
 146         /*
 147          * keep track of the current number of entries in the shared
 148          * memory for new entry detection purposes. It's possible for
 149          * the data structure to be modified while the Map is being
 150          * built and the entry count in the header might change while
 151          * we are parsing it. The map will contain all the counters
 152          * found, but the number recorded in numEntries might be small
 153          * than what than the number we actually parsed (due to asynchronous
 154          * updates). This discrepency is handled by ignoring any re-parsed
 155          * entries when updating the Map in getNewMonitors().
 156          */
 157         lastNumEntries = numEntries;
 158 
 159         // keep track of the monitors just added.
 160         insertedMonitors = new ArrayList<Monitor>(map.values());
 161     }
 162 
 163     /**
 164      * {@inheritDoc}
 165      */
 166     protected void getNewMonitors(Map<String, Monitor> map) throws MonitorException {
 167         assert Thread.holdsLock(this);
 168 
 169         int numEntries = prologue.getNumEntries();
 170 
 171         if (numEntries > lastNumEntries) {
 172             lastNumEntries = numEntries;
 173             Monitor monitor = getNextMonitorEntry();
 174 
 175             while (monitor != null) {
 176                 String name = monitor.getName();
 177 
 178                 // guard against re-parsed entries
 179                 if (!map.containsKey(name)) {
 180                     map.put(name, monitor);
 181                     if (insertedMonitors != null) {
 182                         insertedMonitors.add(monitor);
 183                     }
 184                 }
 185                 monitor = getNextMonitorEntry();
 186             }
 187         }
 188     }
 189 
 190     /**
 191      * {@inheritDoc}
 192      */
 193     protected MonitorStatus getMonitorStatus(Map<String, Monitor> map) throws MonitorException {
 194         assert Thread.holdsLock(this);
 195         assert insertedMonitors != null;
 196 
 197         // load any new monitors
 198         getNewMonitors(map);
 199 
 200         // current implementation doesn't support deletion of reuse of entries
 201         ArrayList removed = EMPTY_LIST;
 202         ArrayList inserted = insertedMonitors;
 203 
 204         insertedMonitors = new ArrayList<Monitor>();
 205         return new MonitorStatus(inserted, removed);
 206     }
 207 
 208     /**
 209      * Build the pseudo monitors used to map the prolog data into counters.
 210      */
 211     protected void buildPseudoMonitors(Map<String, Monitor> map) {
 212         Monitor monitor = null;
 213         String name = null;
 214         IntBuffer ib = null;
 215 
 216         name = PerfDataBufferPrologue.PERFDATA_MAJOR_NAME;
 217         ib = prologue.majorVersionBuffer();
 218         monitor = new PerfIntegerMonitor(name, Units.NONE,
 219                                          Variability.CONSTANT, false, ib);
 220         map.put(name, monitor);
 221 
 222         name = PerfDataBufferPrologue.PERFDATA_MINOR_NAME;
 223         ib = prologue.minorVersionBuffer();
 224         monitor = new PerfIntegerMonitor(name, Units.NONE,
 225                                          Variability.CONSTANT, false, ib);
 226         map.put(name, monitor);
 227 
 228         name = PerfDataBufferPrologue.PERFDATA_BUFFER_SIZE_NAME;
 229         ib = prologue.sizeBuffer();
 230         monitor = new PerfIntegerMonitor(name, Units.BYTES,
 231                                          Variability.MONOTONIC, false, ib);
 232         map.put(name, monitor);
 233 
 234         name = PerfDataBufferPrologue.PERFDATA_BUFFER_USED_NAME;
 235         ib = prologue.usedBuffer();
 236         monitor = new PerfIntegerMonitor(name, Units.BYTES,
 237                                          Variability.MONOTONIC, false, ib);
 238         map.put(name, monitor);
 239 
 240         name = PerfDataBufferPrologue.PERFDATA_OVERFLOW_NAME;
 241         ib = prologue.overflowBuffer();
 242         monitor = new PerfIntegerMonitor(name, Units.BYTES,
 243                                          Variability.MONOTONIC, false, ib);
 244         map.put(name, monitor);
 245         this.overflow = (IntegerMonitor)monitor;
 246 
 247         name = PerfDataBufferPrologue.PERFDATA_MODTIMESTAMP_NAME;
 248         LongBuffer lb = prologue.modificationTimeStampBuffer();
 249         monitor = new PerfLongMonitor(name, Units.TICKS,
 250                                       Variability.MONOTONIC, false, lb);
 251         map.put(name, monitor);
 252     }
 253 
 254     /**
 255      * Method that waits until the target jvm indicates that
 256      * its shared memory is safe to access.
 257      */
 258     protected void synchWithTarget() throws MonitorException {
 259         /*
 260          * synch must happen with syncWaitMs from now. Default is 5 seconds,
 261          * which is reasonabally generous and should provide for extreme
 262          * situations like startup delays due to allocation of large ISM heaps.
 263          */
 264         long timeLimit = System.currentTimeMillis() + syncWaitMs;
 265 
 266         // loop waiting for the accessible indicater to be non-zero
 267         log("synchWithTarget: " + lvmid + " ");
 268         while (!prologue.isAccessible()) {
 269 
 270             log(".");
 271 
 272             // give the target jvm a chance to complete initializatoin
 273             try { Thread.sleep(20); } catch (InterruptedException e) { }
 274 
 275             if (System.currentTimeMillis() > timeLimit) {
 276                 logln("failed: " + lvmid);
 277                 throw new MonitorException("Could not synchronize with target");
 278             }
 279         }
 280         logln("success: " + lvmid);
 281     }
 282 
 283     /**
 284      * method to extract the next monitor entry from the instrumentation memory.
 285      * assumes that nextEntry is the offset into the byte array
 286      * at which to start the search for the next entry. method leaves
 287      * next entry pointing to the next entry or to the end of data.
 288      */
 289     protected Monitor getNextMonitorEntry() throws MonitorException {
 290         Monitor monitor = null;
 291 
 292         // entries are always 4 byte aligned.
 293         if ((nextEntry % 4) != 0) {
 294             throw new MonitorStructureException(
 295                     "Misaligned entry index: "
 296                     + Integer.toHexString(nextEntry));
 297         }
 298 
 299         // protect againt a corrupted shard memory region.
 300         if ((nextEntry < 0)  || (nextEntry > buffer.limit())) {
 301             throw new MonitorStructureException(
 302                     "Entry index out of bounds: "
 303                     + Integer.toHexString(nextEntry)
 304                     + ", limit = " + Integer.toHexString(buffer.limit()));
 305         }
 306 
 307         // check for end of the buffer
 308         if (nextEntry == buffer.limit()) {
 309             logln("getNextMonitorEntry():"
 310                   + " nextEntry == buffer.limit(): returning");
 311             return null;
 312         }
 313 
 314         buffer.position(nextEntry);
 315 
 316         int entryStart = buffer.position();
 317         int entryLength = buffer.getInt();
 318 
 319         // check for valid entry length
 320         if ((entryLength < 0) || (entryLength > buffer.limit())) {
 321             throw new MonitorStructureException(
 322                     "Invalid entry length: entryLength = " + entryLength
 323                     + " (0x" + Integer.toHexString(entryLength) + ")");
 324         }
 325 
 326         // check if last entry occurs before the eof.
 327         if ((entryStart + entryLength) > buffer.limit()) {
 328             throw new MonitorStructureException(
 329                     "Entry extends beyond end of buffer: "
 330                     + " entryStart = 0x" + Integer.toHexString(entryStart)
 331                     + " entryLength = 0x" + Integer.toHexString(entryLength)
 332                     + " buffer limit = 0x" + Integer.toHexString(buffer.limit()));
 333         }
 334 
 335         if (entryLength == 0) {
 336             // end of data
 337             return null;
 338         }
 339 
 340         // we can safely read this entry
 341         int nameOffset = buffer.getInt();
 342         int vectorLength = buffer.getInt();
 343         byte typeCodeByte = buffer.get();
 344         byte flags = buffer.get();
 345         byte unitsByte = buffer.get();
 346         byte varByte = buffer.get();
 347         int dataOffset = buffer.getInt();
 348 
 349         dump_entry_fixed(entryStart, nameOffset, vectorLength, typeCodeByte,
 350                          flags, unitsByte, varByte, dataOffset);
 351 
 352         // convert common attributes to their object types
 353         Units units = Units.toUnits(unitsByte);
 354         Variability variability = Variability.toVariability(varByte);
 355         TypeCode typeCode = null;
 356         boolean supported = (flags & 0x01) != 0;
 357 
 358         try {
 359             typeCode = TypeCode.toTypeCode(typeCodeByte);
 360 
 361         } catch (IllegalArgumentException e) {
 362             throw new MonitorStructureException(
 363                     "Illegal type code encountered:"
 364                     + " entry_offset = 0x" + Integer.toHexString(nextEntry)
 365                     + ", type_code = " + Integer.toHexString(typeCodeByte));
 366         }
 367 
 368         // verify that the name_offset is contained within the entry bounds
 369         if (nameOffset > entryLength) {
 370             throw new MonitorStructureException(
 371                     "Field extends beyond entry bounds"
 372                     + " entry_offset = 0x" + Integer.toHexString(nextEntry)
 373                     + ", name_offset = 0x" + Integer.toHexString(nameOffset));
 374         }
 375 
 376         // verify that the data_offset is contained within the entry bounds
 377         if (dataOffset > entryLength) {
 378             throw new MonitorStructureException(
 379                     "Field extends beyond entry bounds:"
 380                     + " entry_offset = 0x" + Integer.toHexString(nextEntry)
 381                     + ", data_offset = 0x" + Integer.toHexString(dataOffset));
 382         }
 383 
 384         // validate the variability and units fields
 385         if (variability == Variability.INVALID) {
 386             throw new MonitorDataException(
 387                     "Invalid variability attribute:"
 388                     + " entry_offset = 0x" + Integer.toHexString(nextEntry)
 389                     + ", variability = 0x" + Integer.toHexString(varByte));
 390         }
 391 
 392         if (units == Units.INVALID) {
 393             throw new MonitorDataException(
 394                     "Invalid units attribute: entry_offset = 0x"
 395                     + Integer.toHexString(nextEntry)
 396                     + ", units = 0x" + Integer.toHexString(unitsByte));
 397         }
 398 
 399         // the entry looks good - parse the variable length components
 400 
 401         /*
 402          * The name starts at nameOffset and continues up to the first null
 403          * byte. however, we don't know the length, but we can approximate it
 404          * without searching for the null by using the offset for the data
 405          * field, which follows the name field.
 406          */
 407         assert (buffer.position() == (entryStart + nameOffset));
 408         assert (dataOffset > nameOffset);
 409 
 410         // include possible pad space
 411         int maxNameLength = dataOffset-nameOffset;
 412 
 413         // maxNameLength better be less than the total entry length
 414         assert (maxNameLength < entryLength);
 415 
 416         // collect the characters, but do not collect the null byte,
 417         // as the String(byte[]) constructor does not ignore it!
 418         byte[] nameBytes = new byte[maxNameLength];
 419         int nameLength = 0;
 420         byte b;
 421         while (((b = buffer.get()) != 0) && (nameLength < maxNameLength)) {
 422              nameBytes[nameLength++] = b;
 423         }
 424 
 425         assert (nameLength < maxNameLength);
 426 
 427         // we should before or at the start of the data field
 428         assert (buffer.position() <= (entryStart + dataOffset));
 429 
 430         // convert the name bytes into a String
 431         String name = new String(nameBytes, 0, nameLength);
 432 
 433         /*
 434          * compute the size of the data item - this includes pad
 435          * characters used to align the next entry.
 436          */
 437         int dataSize = entryLength - dataOffset;
 438 
 439         // set the position to the start of the data item
 440         buffer.position(entryStart + dataOffset);
 441 
 442         dump_entry_variable(name, buffer, dataSize);
 443 
 444         if (vectorLength == 0) {
 445             // create a scalar Monitor object
 446             if (typeCode == TypeCode.LONG) {
 447                 LongBuffer lb = buffer.asLongBuffer();
 448                 lb.limit(1);  // limit buffer size to one long value.
 449                 monitor = new PerfLongMonitor(name, units, variability,
 450                                               supported, lb);
 451             } else {
 452                 /*
 453                  * unexpected type code - coding error or uncoordinated
 454                  * JVM change
 455                  */
 456                 throw new MonitorTypeException(
 457                         "Unexpected type code encountered:"
 458                         + " entry_offset = 0x" + Integer.toHexString(nextEntry)
 459                         + ", name = " + name
 460                         + ", type_code = " + typeCode
 461                         + " (0x" + Integer.toHexString(typeCodeByte) + ")");
 462             }
 463         } else {
 464             // create a vector Monitor object
 465             if (typeCode == TypeCode.BYTE) {
 466                 if (units != Units.STRING) {
 467                     // only byte arrays of type STRING are currently supported
 468                     throw new MonitorTypeException(
 469                             "Unexpected vector type encounterd:"
 470                             + " entry_offset = "
 471                             + Integer.toHexString(nextEntry)
 472                             + ", name = " + name
 473                             + ", type_code = " + typeCode + " (0x"
 474                             + Integer.toHexString(typeCodeByte) + ")"
 475                             + ", units = " + units + " (0x"
 476                             + Integer.toHexString(unitsByte) + ")");
 477                 }
 478 
 479                 ByteBuffer bb = buffer.slice();
 480                 bb.limit(vectorLength); // limit buffer length to # of chars
 481 
 482                 if (variability == Variability.CONSTANT) {
 483                     monitor = new PerfStringConstantMonitor(name, supported,
 484                                                             bb);
 485                 } else if (variability == Variability.VARIABLE) {
 486                     monitor = new PerfStringVariableMonitor(name, supported,
 487                                                             bb, vectorLength-1);
 488                 } else if (variability == Variability.MONOTONIC) {
 489                     // Monotonically increasing byte arrays are not supported
 490                     throw new MonitorDataException(
 491                             "Unexpected variability attribute:"
 492                             + " entry_offset = 0x"
 493                             + Integer.toHexString(nextEntry)
 494                             + " name = " + name
 495                             + ", variability = " + variability + " (0x"
 496                             + Integer.toHexString(varByte) + ")");
 497                 } else {
 498                     // variability was validated above, so this unexpected
 499                     assert false;
 500                 }
 501             } else {
 502                 // coding error or uncoordinated JVM change
 503                 throw new MonitorTypeException(
 504                         "Unexpected type code encountered:"
 505                         + " entry_offset = 0x"
 506                         + Integer.toHexString(nextEntry)
 507                         + ", name = " + name
 508                         + ", type_code = " + typeCode + " (0x"
 509                         + Integer.toHexString(typeCodeByte) + ")");
 510             }
 511         }
 512 
 513         // setup index to next entry for next iteration of the loop.
 514         nextEntry = entryStart + entryLength;
 515         return monitor;
 516     }
 517 
 518     /**
 519      * Method to dump debugging information
 520      */
 521     private void dumpAll(Map<String, Monitor> map, int lvmid) {
 522         if (DEBUG) {
 523             Set<String> keys = map.keySet();
 524 
 525             System.err.println("Dump for " + lvmid);
 526             int j = 0;
 527             for (Iterator i = keys.iterator(); i.hasNext(); j++) {
 528                 Monitor monitor = map.get(i.next());
 529                 System.err.println(j + "\t" + monitor.getName()
 530                                    + "=" + monitor.getValue());
 531             }
 532             System.err.println("nextEntry = " + nextEntry);
 533             System.err.println("Buffer info:");
 534             System.err.println("buffer = " + buffer);
 535         }
 536     }
 537 
 538     /**
 539      * Method to dump the fixed portion of an entry.
 540      */
 541     private void dump_entry_fixed(int entry_start, int nameOffset,
 542                                   int vectorLength, byte typeCodeByte,
 543                                   byte flags, byte unitsByte, byte varByte,
 544                                   int dataOffset) {
 545         if (DEBUG) {
 546             System.err.println("Entry at offset: 0x"
 547                                + Integer.toHexString(entry_start));
 548             System.err.println("\tname_offset = 0x"
 549                                + Integer.toHexString(nameOffset));
 550             System.err.println("\tvector_length = 0x"
 551                                + Integer.toHexString(vectorLength));
 552             System.err.println("\tdata_type = 0x"
 553                                + Integer.toHexString(typeCodeByte));
 554             System.err.println("\tflags = 0x"
 555                                + Integer.toHexString(flags));
 556             System.err.println("\tdata_units = 0x"
 557                                + Integer.toHexString(unitsByte));
 558             System.err.println("\tdata_variability = 0x"
 559                                + Integer.toHexString(varByte));
 560             System.err.println("\tdata_offset = 0x"
 561                                + Integer.toHexString(dataOffset));
 562         }
 563     }
 564 
 565     private void dump_entry_variable(String name, ByteBuffer bb, int size) {
 566         if (DEBUG) {
 567             char[] toHex = new char[] { '0', '1', '2', '3',
 568                                         '4', '5', '6', '7',
 569                                         '8', '9', 'a', 'b',
 570                                         'c', 'd', 'e', 'f' };
 571 
 572             ByteBuffer data = bb.slice();
 573             data.limit(size);
 574 
 575             System.err.println("\tname = " + name);
 576             System.err.println("\tdata = ");
 577 
 578             int count=0;
 579             while (data.hasRemaining()) {
 580                 byte b = data.get();
 581                 byte high = (byte)((b >> 8) & 0x0f);
 582                 byte low = (byte)(b & 0x0f);
 583 
 584                 if (count % 16 == 0) {
 585                     System.err.print("\t\t" + Integer.toHexString(count / 16)
 586                                      + ": ");
 587                 }
 588 
 589                 System.err.print(String.valueOf(toHex[high])
 590                                  + String.valueOf(toHex[low]));
 591 
 592                 count++;
 593                 if (count % 16 == 0) {
 594                     System.err.println();
 595                 } else {
 596                     System.err.print(" ");
 597                 }
 598             }
 599             if (count % 16 != 0) {
 600                 System.err.println();
 601             }
 602         }
 603     }
 604 
 605     private void logln(String s) {
 606         if (DEBUG) {
 607             System.err.println(s);
 608         }
 609     }
 610 
 611     private void log(String s) {
 612         if (DEBUG) {
 613             System.err.print(s);
 614         }
 615     }
 616 }