All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.it.unimi.dsi.logging.ProgressLogger Maven / Gradle / Ivy

package it.unimi.dsi.logging;

/*
 * DSI utilities
 *
 * Copyright (C) 2005-2017 Sebastiano Vigna
 *
 *  This library is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU Lesser General Public License as published by the Free
 *  Software Foundation; either version 3 of the License, or (at your option)
 *  any later version.
 *
 *  This library 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 Lesser General Public License
 *  for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, see .
 *
 */

import static it.unimi.dsi.Util.RUNTIME;
import it.unimi.dsi.Util;

import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Tunable progress logger.
 *
 * 

This class provides a simple way to log progress information about long-lasting activities. While originally * based on Log4J 1.2, it is currently based on SLF4J. * *

To use this class, you first create a new instance by passing an * {@linkplain #ProgressLogger(Logger,long,TimeUnit,String) SLF4J logger, a time interval and a name for the items that are being logged}. * Information will be logged about the current state of affairs no more often than the given * time interval. Note that the {@linkplain #itemsName name of the logged items} can be changed at any time. * *

To log the progress of an activity, you call {@link #start(CharSequence)} at the beginning, which will * display the given string. Then, each time you want to mark progress, you call usually {@link #update()} or {@link #lightUpdate()}. * The latter methods increase the item counter, and will log progress information if enough time * has passed since the last log (and if the counter is a multiple of {@link #LIGHT_UPDATE_MASK} + 1, in the case of {@link #lightUpdate()}). * Other update methods (e.g., {@link #update(long)}, {@link #updateAndDisplay()}, …) make the update and display process very flexible. * * When the activity is over, you call {@link #stop()}. At that point, the method {@link #toString()} returns * information about the internal state of the logger (elapsed time, number of items per second) that * can be printed or otherwise processed. If {@link #update()} has never been called, you will just * get the elapsed time. By calling {@link #done()} instead of stop, this information will be logged for you. * *

Additionally: *

    *
  • by setting the {@linkplain #expectedUpdates expected amount of updates} before * calling {@link #start()} you can get some estimations on the completion time; *
  • by setting {@link #displayFreeMemory} you can display information about the * memory usage; *
  • by setting {@link #displayLocalSpeed} you can display, beside the average speed since * the start of the activity, the speed since the last log; *
  • by setting {@link #info} you can display arbitrary additional information. *
  • by setting {@link #speedTimeUnit} and {@link #itemTimeUnit} you can fix the time unit * used to measure the speed and the time per item. *
* *

After you finished a run of the progress logger, * you can change its attributes and call {@link #start()} again * to measure another activity. * *

A typical call sequence to a progress logger is as follows: *

 * ProgressLogger pl = new ProgressLogger(logger, 1, TimeUnit.MINUTES);
 * pl.start("Smashing pumpkins...");
 * ... activity on pumpkins that calls update() on each pumpkin ...
 * pl.done();
 * 
* *

A more flexible behaviour can be obtained at the end of the * process by calling {@link #stop()}: *

 * ProgressLogger pl = new ProgressLogger(logger, 1, TimeUnit.MINUTES, "pumpkins");
 * pl.start("Smashing pumpkins...");
 * ... activity on pumpkins that calls update() on each pumpkin ...
 * pl.stop("Really done!");
 * pl.logger.info(pl.toString());
 * 
* *

An instance of this class can also be used as a handy timer: *

 * ProgressLogger pl = new ProgressLogger();
 * pl.start("Smashing pumpkins...");
 * ... activity on pumpkins (no update() calls) ...
 * pl.done(howManyPumpkins);
 * 
* *

Should you need to display additional information, you can set the field {@link #info} to any * object: it will be printed just after the timing (and possibly memory) information. * * @author Sebastiano Vigna * @since 0.9.3 */ public final class ProgressLogger { public final static long ONE_SECOND = 1000; public final static long TEN_SECONDS = 10 * ONE_SECOND; public final static long ONE_MINUTE = ONE_SECOND * 60; public final static long TEN_MINUTES = ONE_MINUTE * 10; public final static long ONE_HOUR = ONE_MINUTE * 60; public final static long DEFAULT_LOG_INTERVAL = TEN_SECONDS; /** An array of pattern defining rules that turn plural into singular (the corresponding replacement string is in the same position in {@link #SINGULAR}). * More specific rules are at the end of the array (and should be tested first). */ private static final Pattern[] PLURAL = { Pattern.compile("s$"), Pattern.compile("(s|si|u)s$"), Pattern.compile("(n)ews$"), Pattern.compile("([ti])a$"), Pattern.compile("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$"), Pattern.compile("(^analy)ses$"), Pattern.compile("(^analy)sis$"), Pattern.compile("([^f])ves$"), Pattern.compile("(hive)s$"), Pattern.compile("(tive)s$"), Pattern.compile("([lr])ves$"), Pattern.compile("([^aeiouy]|qu)ies$"), Pattern.compile("(s)eries$"), Pattern.compile("(m)ovies$"), Pattern.compile("(x|ch|ss|sh)es$"), Pattern.compile("([m|l])ice$"), Pattern.compile("(bus)es$"), Pattern.compile("(o)es$"), Pattern.compile("(shoe)s$"), Pattern.compile("(cris|ax|test)is$"), Pattern.compile("(cris|ax|test)es$"), Pattern.compile("(octop|vir)i$"), Pattern.compile("(octop|vir)us$"), Pattern.compile("(alias|status)es$"), Pattern.compile("(alias|status)$"), Pattern.compile("^(ox)en"), Pattern.compile("(vert|ind)ices$"), Pattern.compile("(matr)ices$"), Pattern.compile("(quiz)zes$"), Pattern.compile("^people$"), Pattern.compile("^men$"), Pattern.compile("^women$"), Pattern.compile("^children$"), Pattern.compile("^sexes$"), Pattern.compile("^moves$"), Pattern.compile("^stadiums$") }; /** Replacement strings for {@link #PLURAL}'s patterns. */ private static final String[] SINGULAR = { "", "$1s", "$1ews", "$1um", "$1$2sis", "$1sis", "$1sis", "$1fe", "$1", "$1", "$1f", "$1y", "$1eries", "$1ovie", "$1", "$1ouse", "$1", "$1", "$1", "$1is", "$1is", "$1us", "$1us", "$1", "$1", "$1", "$1ex", "$1ix", "$1", "person", "man", "woman", "child", "sex", "move", "stadium" }; /** Calls to {@link #lightUpdate()} will cause a call to * {@link System#currentTimeMillis()} only if the current value of {@link #count} * is a multiple of this mask plus one. */ public final int LIGHT_UPDATE_MASK = (1 << 10) - 1; /** The SLF4J logger used by this progress logger. */ public final Logger logger; /** The time interval for a new log in milliseconds. Can be set at any time (e.g., using {@link TimeUnit#toMillis(long)}). */ public long logInterval; /** If non-{@code null}, this object will be printed after the timing information. */ public Object info; /** The number of calls to {@link #update()} since the last {@link #start()} (but it be changed also with {@link #update(long)} and {@link #set(long)}). */ public long count; /** The value of {@link #count} at the last call to {@link #updateInternal(long)} (i.e., at the last output). */ private long lastCount; /** The number of expected calls to {@link #update()} (used to compute the percentages, ignored if negative). */ public long expectedUpdates; /** The name of several counted items. */ public String itemsName; /** Whether to display the free memory at each progress log (default: false). */ public boolean displayFreeMemory; /** Whether to display additionally the local speed, that is, the detected speed between two consecutive logs, as opposed to the average speed since {@link #start()} (default: false). */ public boolean displayLocalSpeed; /** A fixed time unit for printing the speed. If {@code null}, {@link ProgressLogger} will choose a time unit that * is easy to understand. In some cases (e.g., machine-parsed logs) you might want to have always the speed in * items per {@linkplain TimeUnit#SECONDS seconds}, {@linkplain TimeUnit#MINUTES minues}, {@linkplain TimeUnit#HOURS hours} or {@linkplain TimeUnit#DAYS days}. */ public TimeUnit speedTimeUnit; /** A fixed time unit for printing the timing of an item. If {@code null}, {@link ProgressLogger} chooses a unit that * is easy to understand. In some cases (e.g., machine-parsed logs) you might want to have always have the time for * an item in {@linkplain TimeUnit#NANOSECONDS nanoseconds}, {@linkplain TimeUnit#MICROSECONDS milliseconds}, {@linkplain TimeUnit#MILLISECONDS milliseconds}, {@linkplain TimeUnit#SECONDS seconds}, {@linkplain TimeUnit#MINUTES minutes} or {@linkplain TimeUnit#HOURS hours}. */ public TimeUnit itemTimeUnit; /** The name of a single counted item, computed by {@link #singularize()} on {@link #referenceItemsName} and cached (we want to avoid a costly scan of * {@link #SINGULAR}'s patterns at every output). */ private String itemName; /** The value of {@link #itemsName} for which {@link #itemName} is valid. */ private String referenceItemsName; /** The time at the last call to {@link #start()}. */ private long startTime; /** The time at the last call to {@link #stop()}. */ private long stopTime; /** The time of the last log. */ private long lastLogTime; /** Creates a new progress logger using items as items name and logging every * {@value #DEFAULT_LOG_INTERVAL} milliseconds * to the {@linkplain Logger#ROOT_LOGGER_NAME root logger}. */ public ProgressLogger() { this(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)); } /** Creates a new progress logger logging every {@value #DEFAULT_LOG_INTERVAL} milliseconds * to the {@linkplain Logger#ROOT_LOGGER_NAME root logger}. * * @param itemsName a plural name denoting the counted items. */ public ProgressLogger(final String itemsName) { this(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME), itemsName); } /** Creates a new progress logger using items as items name and logging every {@value #DEFAULT_LOG_INTERVAL} milliseconds. * * @param logger the logger to which messages will be sent. */ public ProgressLogger(final Logger logger) { this(logger, DEFAULT_LOG_INTERVAL, TimeUnit.MILLISECONDS); } /** Creates a new progress logger logging every {@value #DEFAULT_LOG_INTERVAL} milliseconds. * * @param logger the logger to which messages will be sent. * @param itemsName a plural name denoting the counted items. */ public ProgressLogger(final Logger logger, final String itemsName) { this(logger, DEFAULT_LOG_INTERVAL, TimeUnit.MILLISECONDS, itemsName); } /** Creates a new progress logger using items as items name. * * @param logger the logger to which messages will be sent. * @param logInterval the logging interval. * @param timeUnit the unit of time of {@code logInterval}. */ public ProgressLogger(final Logger logger, final long logInterval, final TimeUnit timeUnit) { this(logger, logInterval, timeUnit, "items"); } /** Creates a new progress logger. * * @param logger the logger to which messages will be sent. * @param logInterval the logging interval. * @param timeUnit the unit of time of {@code logInterval}. * @param itemsName a plural name denoting the counted items. */ public ProgressLogger(final Logger logger, final long logInterval, final TimeUnit timeUnit, final String itemsName) { this.logger = logger; this.logInterval = timeUnit.toMillis(logInterval); this.itemsName = itemsName; this.expectedUpdates = -1; } /** The SLF4J logger * used by this progress logger; it just returns the content of the field {@link #logger}. */ public Logger logger() { return logger; } private final String itemName() { if (referenceItemsName == itemsName) return itemName; referenceItemsName = itemsName; for(int i = PLURAL.length; i-- != 0;) { final Matcher matcher = PLURAL[i].matcher(itemsName); if (matcher.find()) return itemName = matcher.replaceFirst(SINGULAR[i]); } return itemName = itemsName; } /** Updates the internal count of this progress logger by adding one; if enough time has passed since the * last log, information will be logged. * *

This method is kept intentionally short (it delegates most of the work to an internal * private method) so to suggest inlining. However, it performs a call to {@link System#currentTimeMillis()} * that takes microseconds (not nanoseconds). If you plan on calling this method more than a * few thousands times per second, you should use {@link #lightUpdate()}. */ public void update() { update(1); } /** Updates the internal count of this progress logger by adding a specified value; if enough time has passed since the * last log, information will be logged. * * @see #update() */ public void update(final long count) { this.count += count; final long time = System.currentTimeMillis(); if (time - lastLogTime >= logInterval) updateInternal(time); } /** Updates the internal count of this progress logger by adding one, forcing a display. * * @see #update() */ public void updateAndDisplay() { updateAndDisplay(1); } /** Sets the internal count of this progress logger to a specified value; if enough time has passed since the * last log, information will be logged. * * @see #update() */ public void set(final long count) { this.count = count; final long time = System.currentTimeMillis(); if (time - lastLogTime >= logInterval) updateInternal(time); } /** Sets the internal count of this progress logger to a specified value, forcing a display. * * @see #update() */ public void setAndDisplay(final long count) { this.count = count; updateInternal(System.currentTimeMillis()); } /** Updates the internal count of this progress logger by adding a specified value, forcing a display. * * @see #update() */ public void updateAndDisplay(final long count) { this.count += count; updateInternal(System.currentTimeMillis()); } private String freeMemory() { return (displayFreeMemory ? "; used/avail/free/total/max mem: " + Util.formatSize(RUNTIME.totalMemory() - RUNTIME.freeMemory()) + "/" + Util.formatSize(RUNTIME.freeMemory() + (RUNTIME.maxMemory() - RUNTIME.totalMemory())) + "/" + Util.formatSize(RUNTIME.freeMemory()) + "/" + Util.formatSize(RUNTIME.totalMemory()) + "/" + Util.formatSize(RUNTIME.maxMemory()) : ""); } private String itemsPerTimeInterval(long startCount, final long currentTime, long baseTime) { final double secondsPerItem = ((count - startCount) * 1000.0) / (currentTime - baseTime); if (speedTimeUnit == TimeUnit.SECONDS || speedTimeUnit == null && secondsPerItem >= 1) return Util.format(secondsPerItem) + " " + itemsName + "/s"; if (speedTimeUnit == TimeUnit.MINUTES || speedTimeUnit == null && secondsPerItem * 60 >= 1) return Util.format(secondsPerItem * 60) + " " + itemsName + "/m"; if (speedTimeUnit == TimeUnit.HOURS || speedTimeUnit == null && secondsPerItem * 3600 >= 1) return Util.format(secondsPerItem * 3600) + " " + itemsName + "/h"; return Util.format(secondsPerItem * 86400) + " " + itemsName + "/d"; } private String timePerItem(long startCount, final long currentTime, long baseTime) { final double secondsPerItem = (currentTime - baseTime) / ((count - startCount) * 1000.0); if (itemTimeUnit == null && secondsPerItem >= 86400) return Util.format(secondsPerItem / 86400) + " d/" + itemName(); if (itemTimeUnit == TimeUnit.HOURS || itemTimeUnit == null && secondsPerItem >= 3600) return Util.format(secondsPerItem / 3600) + " h/" + itemName(); if (itemTimeUnit == TimeUnit.MINUTES || itemTimeUnit == null && secondsPerItem >= 60) return Util.format(secondsPerItem / 60) + " m/" + itemName(); if (itemTimeUnit == TimeUnit.SECONDS || itemTimeUnit == null && secondsPerItem >= 1) return Util.format(secondsPerItem) + " s/" + itemName(); if (itemTimeUnit == TimeUnit.MILLISECONDS || itemTimeUnit == null && secondsPerItem >= 1E-3) return Util.format(secondsPerItem * 1E3) + " ms/" + itemName(); if (itemTimeUnit == TimeUnit.MICROSECONDS || itemTimeUnit == null && secondsPerItem >= 1E-6) return Util.format(secondsPerItem * 1E6) + " \u00b5s/" + itemName(); return Util.format(secondsPerItem * 1E9) + " ns/" + itemName(); } private void updateInternal(final long currentTime) { final long millisToEnd = Math.round((expectedUpdates - count) * ((currentTime - startTime) / (count + 1.0))); // Formatting is expensive, so we check for actual logging. if (logger().isInfoEnabled()) logger().info(Util.format(count) + " " + itemsName + ", " + millis2hms(millis()) + ", " + itemsPerTimeInterval(0, currentTime, startTime) + ", " + timePerItem(0, currentTime, startTime) + (displayLocalSpeed ? " [" + itemsPerTimeInterval(lastCount, currentTime, lastLogTime) + ", " + timePerItem(lastCount, currentTime, lastLogTime) + "]" : "") + (expectedUpdates > 0 ? "; " + Util.format((100 * count) / expectedUpdates) + "% done, " + millis2hms(millisToEnd) + " to end" : "") + freeMemory() + (info != null ? "; " + info : "")); lastLogTime = currentTime; lastCount = count; return; } /** Updates the internal count of this progress logger by adding one in a lightweight fashion. * *

This call updates the progress logger internal counter as {@link #update()}. However, * it will actually call {@link System#currentTimeMillis()} only if the new {@link #count} * is a multiple of {@link #LIGHT_UPDATE_MASK} + 1. This mechanism makes it possible to reduce the number of * calls to {@link System#currentTimeMillis()} significantly. * *

This method is useful when the operations being counted take less than a few microseconds. * * @see #update() */ public final void lightUpdate() { if ((++count & LIGHT_UPDATE_MASK) == 0) { final long time = System.currentTimeMillis(); if (time - lastLogTime >= logInterval) updateInternal(time); } } /** Starts the progress logger, resetting the count.*/ public void start() { start(null, 0); } /** Starts the progress logger, displaying a message and resetting the count. * * @param message the message to display. */ public void start(final CharSequence message) { start(message, 0); } /** Starts the progress logger, resetting the count and assuming that a given amount of time has already passed. * * @param alreadyElapsed the number of milliseconds already elapsed. * @see #start(CharSequence, long) */ public void start(final long alreadyElapsed) { start(null, alreadyElapsed); } /** Starts the progress logger, displaying a message, resetting the count and assuming that a given amount of time has already passed. * *

The effect of the {@code alreadyElapsed} parameter is that the start time of this {@link ProgressLogger} will be * set to {@link System#currentTimeMillis()} minus {@code alreadyElapsed}. * * @param message the message to display. * @param alreadyElapsed the number of milliseconds already elapsed. */ public void start(final CharSequence message, final long alreadyElapsed) { if (message != null) logger().info(message.toString()); startTime = (lastLogTime = System.currentTimeMillis()) - alreadyElapsed; lastCount = count = 0; stopTime = -1; } /** Stops the progress logger, displaying a message. * *

This method will also mark {@link #expectedUpdates} as invalid, * to avoid erroneous reuses of previous values. * * @param message the message to display. */ public void stop(final CharSequence message) { if (stopTime != -1) return; if (message != null) logger().info(message.toString()); stopTime = System.currentTimeMillis(); expectedUpdates = -1; } /** Stops the progress logger. */ public void stop() { stop(null); } /** Completes a run of this progress logger, logging “Completed.” and the logger itself. */ public void done() { stop("Completed."); logger().info(toString()); } /** Completes a run of this progress logger and sets the internal counter, logging “Completed.” and the logger itself. * *

This method is particularly useful in two circumstances: *

    *
  • you have updated the logger with some approximate values (e.g., in a multicore computation) but before * printing the final statistics you want the internal counter to contain an exact value; *
  • you have used the logger as a handy timer, calling just {@link #start()} and this method. *
* * @param count will replace the internal counter value. */ public void done(long count) { this.count = count; stop("Completed."); logger().info(toString()); } /** Returns the number of milliseconds between present time and the last call to {@link #start()}, if * this progress logger is running, or between the last call to {@link #stop()} and the last call to {@link #start()}, if this * progress logger is stopped. * * @return the number of milliseconds between present time and the last call to {@link #start()}, if * this progress logger is running, or between the last call to {@link #stop()} and the last call to {@link #start()}, if this * progress logger is stopped. */ public long millis() { if (stopTime != -1) return stopTime - startTime; else return System.currentTimeMillis() - startTime; } private String millis2hms(final long t) { if (t < 1000) return t + "ms"; final long s = (t / 1000) % 60; final long m = ((t / 1000) / 60) % 60; final long h = t / (3600 * 1000); if (h == 0 && m == 0) return s + "s"; if (h == 0) return m + "m " + s + "s"; return h + "h " + m + "m " + s + "s"; } /** Converts the data currently stored in this progress logger to a string. * *

If this progress logger has been {@linkplain #stop() stopped}, statistics * are computed using the stop time. Otherwise, they are computed using the current time (i.e., the method call time). * * @return the current data in this progress logger in a printable form. */ @Override public String toString() { final long t = (stopTime != -1 ? stopTime : System.currentTimeMillis()) - startTime + 1 ; if (t <= 0) return "Illegal progress logger state"; return "Elapsed: " + millis2hms(t) + (count != 0 ? " [" + Util.format(count) + " " + itemsName + ", " + itemsPerTimeInterval(0, stopTime, startTime) + ", " + timePerItem(0, stopTime, startTime) + "]" : "") + freeMemory(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy