# HG changeset patch # Parent b200079cd5ac4b5c7f12fdcba716818c484d7aa7 diff --git a/core.startup/src/org/netbeans/core/startup/logging/PrintStreamLogger.java b/core.startup/src/org/netbeans/core/startup/logging/PrintStreamLogger.java --- a/core.startup/src/org/netbeans/core/startup/logging/PrintStreamLogger.java +++ b/core.startup/src/org/netbeans/core/startup/logging/PrintStreamLogger.java @@ -51,8 +51,52 @@ /** a stream to delegate to logging. */ public final class PrintStreamLogger extends PrintStream implements Runnable { + /** + * Threshold of buffered data before the background thread starts to flush it to the logger. + */ + // not private bcs of test + static final int BUFFER_THRESHOLD = 2 * 1024 * 1024; + + /** + * Minimum capacity, does not shring below this size. + */ + private static final int BUFFER_MIN_CAPACITY = 1024 * 1024; + + /** + * Time after which the buffer shrinks/is recycled, if not adequately used. + */ + // not private bcs of test + static final int BUFFER_SHRINK_TIME = 10 * 1000; + + /** + * Ratio of required buffer usage. If the buffer is used up to less than + * 1/n, the builder will be recycled on the next flush. + */ + private static final int BUFFER_CAPACITY_DIV = 5; + private Logger log; - private final StringBuilder sb = new StringBuilder(); + + @SuppressWarnings("RedundantStringConstructorCall") // prevent interning + private final Object lock = new String("lock"); // NOI18N + + // @GuardedBy(lock) + private StringBuilder sb = new StringBuilder(); + + // @GuardedBy(lock) + private int bufferTop = 0; + + /** + * Maximum useful buffer size at the time of flush + * @GuardedBy(lock) + */ + private int maxBufferSize; + + /** + * Last time the buffer was checked/shrinked + * @GuardedBy(lock) + */ + private long lastCleanupTime; + private static RequestProcessor RP = new RequestProcessor("StdErr Flush"); private RequestProcessor.Task flush = RP.create(this, true); @@ -93,10 +137,10 @@ if (RP.isRequestProcessorThread()) { return; } - synchronized (sb) { + synchronized (lock) { sb.append((char) b); + checkFlush(); } - checkFlush(); } @Override @@ -104,10 +148,10 @@ if (NbLogging.DEBUG != null && !NbLogging.wantsMessage(s)) { new Exception().printStackTrace(NbLogging.DEBUG); } - synchronized (sb) { + synchronized (lock) { sb.append(s); + checkFlush(); } - checkFlush(); } @Override @@ -125,7 +169,7 @@ @Override public void flush() { boolean empty; - synchronized (sb) { + synchronized (lock) { empty = sb.length() == 0; } if (!empty) { @@ -142,8 +186,9 @@ private void checkFlush() { //if (DEBUG != null) DEBUG.println("checking flush; buffer: " + sb); // NOI18N + boolean immediate = sb.length() - bufferTop > BUFFER_THRESHOLD; try { - flush.schedule(100); + flush.schedule(immediate ? 0 : 100); } catch (IllegalStateException ex) { /* can happen during shutdown: Nested Exception is: @@ -169,26 +214,67 @@ */ } } + + private final RequestProcessor.Task cleanupTask = RP.create(new Runnable() { + public void run() { + synchronized (lock) { + maybeFreeBuffer(); + } + } + }); + + /** + * Conditionally shrinks the buffer. Called synchronously from flusher, and + * scheduled (by the flusher) to cover the case logging ends within BUFFER_SHRINK_TIME + * and no more data comes. + *

+ * The intention is to recycle the buffer, if it was not filled at least to 1/BUFFER_CAPACITY_DIV + * of its capacity during the last BUFFER_SHRINK_TIME millis. + */ + private void maybeFreeBuffer() { + int c = sb.capacity(); + long t = System.currentTimeMillis(); + if (t - lastCleanupTime > BUFFER_SHRINK_TIME) { + if (c > BUFFER_MIN_CAPACITY && + c / BUFFER_CAPACITY_DIV > maxBufferSize) { + // recycle the buffer, reclaim potential memory + sb = new StringBuilder(sb); + } + maxBufferSize = sb.length(); + lastCleanupTime = t; + } + } public void run() { for (;;) { String toLog; - synchronized (sb) { + synchronized (lock) { if (sb.length() == 0) { break; } + maxBufferSize = Math.max(maxBufferSize, sb.length()); int last = -1; - for (int i = sb.length() - 1; i >= 0; i--) { + for (int i = sb.length() - 1; i >= bufferTop; i--) { if (sb.charAt(i) == '\n') { last = i; break; } } if (last == -1) { + // no \n in the buffer + bufferTop = sb.length(); break; } toLog = sb.substring(0, last + 1); + // the remained does not contain a newline + // PEDNING: perf: circular buffer should be used here. sb.delete(0, last + 1); + bufferTop = sb.length(); + // attempt to free the buffer + maybeFreeBuffer(); + // schedule for the case that data stops coming in + cleanupTask.schedule(BUFFER_SHRINK_TIME); + lock.notifyAll(); } int begLine = 0; while (begLine < toLog.length()) { @@ -199,4 +285,10 @@ } } + // Testing only + int[] bufferSizes() { + synchronized (lock) { + return new int[] { sb.length(), sb.capacity() }; + } + } } // end of LgStream diff --git a/core.startup/test/unit/src/org/netbeans/core/startup/logging/PrintStreamLoggerTest.java b/core.startup/test/unit/src/org/netbeans/core/startup/logging/PrintStreamLoggerTest.java new file mode 100644 --- /dev/null +++ b/core.startup/test/unit/src/org/netbeans/core/startup/logging/PrintStreamLoggerTest.java @@ -0,0 +1,160 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2017 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2017 Sun Microsystems, Inc. + */ +package org.netbeans.core.startup.logging; + +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import static org.netbeans.core.startup.logging.PrintStreamLogger.BUFFER_SHRINK_TIME; +import static org.netbeans.core.startup.logging.PrintStreamLogger.BUFFER_THRESHOLD; +import org.netbeans.junit.NbTestCase; + +/** + * + * @author sdedic + */ +public class PrintStreamLoggerTest extends NbTestCase { + + private static class LH extends Handler { + List records = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + } + + private LH handler = new LH(); + + private Logger logger = Logger.getLogger("test.pslogger"); + + private PrintStreamLogger ps = (PrintStreamLogger)PrintStreamLogger.create("test.pslogger"); + + private Object psLock; + + public PrintStreamLoggerTest(String name) { + super(name); + } + + public void setUp() throws Exception { + logger.addHandler(handler); + logger.setLevel(Level.INFO); + logger.setUseParentHandlers(false); + Field f = ps.getClass().getDeclaredField("lock"); + f.setAccessible(true); + psLock = f.get(ps); + } + + public void tearDown() { + LogManager.getLogManager().getLogger("test.pslogger").removeHandler(handler); + } + + public void testContinousLogingFlushes() throws Exception { + int count = 0; + StringBuilder data = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + data.append("12345678901234567890123456789012345678901234567890\n"); + } + do { + Thread.sleep(50); + if (count < (BUFFER_THRESHOLD / data.length())) { + assertEquals("Not buffering properly", 0, handler.records.size()); + } + // 50k chars + newline + ps.println(data); + } while (++count < ((BUFFER_THRESHOLD / data.length()) * 2)); + assertFalse(handler.records.isEmpty()); + } + + public void testMemoryReclaimed() throws Exception { + int count = 0; + StringBuilder data = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + data.append("12345678901234567890123456789012345678901234567890"); + } + + do { + Thread.sleep(50); + if (count < (BUFFER_THRESHOLD / data.length())) { + assertEquals("Not buffering properly", 0, handler.records.size()); + } + // 50k chars + newline + ps.println(data); + } while (++count < ((BUFFER_THRESHOLD / data.length()) * 2)); + // check that the logger has started to flush already + assertFalse(handler.records.isEmpty()); + + // wait until flush, check that the buffer is still large. + synchronized (psLock) { + ps.println(data); + + // wait & release the lock: + psLock.wait(1000); + + int[] capacity = ps.bufferSizes(); + assertTrue(capacity[1] >= BUFFER_THRESHOLD); + } + Thread.sleep(5 * BUFFER_SHRINK_TIME / 2); + synchronized (psLock) { + ps.println(data); + + // wait & release the lock: + psLock.wait(1000); + } + int[] capacity = ps.bufferSizes(); + assertTrue(capacity[1] < BUFFER_THRESHOLD); + } +}