/* This file is part of VoltDB.
* Copyright (C) 2008-2014 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.join;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import org.voltcore.logging.VoltLogger;
import org.voltdb.StatsSource;
import org.voltdb.VoltTable.ColumnInfo;
import org.voltdb.VoltType;
import org.voltdb.utils.MiscUtils;
import com.google_voltpatches.common.collect.Maps;
public class BalancePartitionsStatistics extends StatsSource {
private static final VoltLogger log = new VoltLogger("JOIN");
private static long logIntervalNanos = TimeUnit.SECONDS.toNanos(120);
long totalRangeSize;
long lastReportTime;
long lastBalanceDuration = 0;
long balanceStart = 0;
// Bytes transferred in each @BalancePartitions call in the past second.
// Keyed by nanosecond timestamp.
TreeMap<Long, Long> bytesTransferredInLastSec = Maps.newTreeMap();
long throughput = 0;
long lastTransferTimeNanos = 0;
private volatile StatsPoint statsPoint;
private StatsPoint intervalStats;
private StatsPoint overallStats;
public BalancePartitionsStatistics()
{
this(0L);
}
public BalancePartitionsStatistics(long totalRangeSize)
{
super(false);
initialize(totalRangeSize);
}
public void initialize(long totalRangeSize)
{
this.overallStats = new StatsPoint("Overall", totalRangeSize);
this.totalRangeSize = totalRangeSize;
this.lastReportTime = overallStats.getStartTimeNanos();
this.lastBalanceDuration = 0;
startInterval();
this.statsPoint = new StatsPoint("Point", totalRangeSize);
this.bytesTransferredInLastSec.clear();
}
public void logBalanceStarts()
{
balanceStart = System.nanoTime();
}
public void logBalanceEnds(long rangeSizeMoved, long bytesTransferred, long callTimeNanos, long transferTimeNanos, long rowsTransferred)
{
final long balanceEnd = System.nanoTime();
lastBalanceDuration = balanceEnd - balanceStart;
final long now = System.nanoTime();
final long aSecondAgo = now - TimeUnit.SECONDS.toNanos(1);
bytesTransferredInLastSec.put(now, bytesTransferred);
// remove entries older than a second
throughput += bytesTransferred;
while (bytesTransferredInLastSec.firstKey() < aSecondAgo) {
throughput -= bytesTransferredInLastSec.pollFirstEntry().getValue();
}
lastTransferTimeNanos = transferTimeNanos;
overallStats = overallStats.update(balanceEnd,
lastBalanceDuration,
callTimeNanos,
transferTimeNanos,
rangeSizeMoved,
rowsTransferred,
bytesTransferred,
1);
intervalStats = intervalStats.update(balanceEnd,
lastBalanceDuration,
callTimeNanos,
transferTimeNanos,
rangeSizeMoved,
rowsTransferred,
bytesTransferred,
1);
markStatsPoint();
// Close out the interval and log statistics every logIntervalSeconds seconds.
if (now - lastReportTime > logIntervalNanos && now != lastReportTime) {
lastReportTime = now;
endInterval();
}
}
public long getThroughput()
{
return throughput;
}
private void startInterval()
{
this.intervalStats = new StatsPoint("Interval", totalRangeSize);
}
private void endInterval()
{
printLog();
}
public void printLog()
{
if (this.bytesTransferredInLastSec.isEmpty()) {
log.info("No data has been migrated yet.");
}
else {
log.info(String.format("JOIN PROGRESS SUMMARY: "
+ "time elapsed: %s "
+ "amount completed: %.2f%% "
+ "est. time remaining: %s",
this.overallStats.getFormattedDuration(),
this.overallStats.getCompletedFraction() * 100.0,
this.overallStats.getFormattedEstimatedRemaining()));
log.info(String.format("JOIN DIAGNOSTICS: %s", intervalStats.toString()));
log.info(String.format("JOIN DIAGNOSTICS: %s", overallStats.toString()));
}
// Immediately start the next interval.
this.startInterval();
}
// Mainly for testing.
public StatsPoint getOverallStats()
{
return this.overallStats;
}
// Mainly for testing.
public StatsPoint getIntervalStats()
{
return this.intervalStats;
}
public StatsPoint getLastStatsPoint()
{
return this.statsPoint;
}
private void markStatsPoint()
{
if (!bytesTransferredInLastSec.isEmpty()) {
statsPoint = overallStats.capture(
"Point",
bytesTransferredInLastSec.lastKey());
}
}
public static interface Constants
{
public final static String TIMESTAMP = "TIMESTAMP";
public final static String PERCENTAGE_MOVED = "PERCENTAGE_MOVED";
public final static String MOVED_ROWS = "MOVED_ROWS";
public final static String ROWS_PER_SECOND = "ROWS_PER_SECOND";
public final static String ESTIMATED_REMAINING = "ESTIMATED_REMAINING";
public final static String MEGABYTES_PER_SECOND = "MEGABYTES_PER_SECOND";
public final static String CALLS_PER_SECOND = "CALLS_PER_SECOND";
public final static String CALLS_LATENCY = "CALLS_LATENCY";
public final static String CALLS_TIME = "CALLS_TIME";
public final static String CALLS_TRANSFER_TIME = "CALLS_TRANSFER_TIME";
}
@Override
protected void populateColumnSchema(ArrayList<ColumnInfo> columns)
{
columns.add(new ColumnInfo(Constants.TIMESTAMP, VoltType.BIGINT));
columns.add(new ColumnInfo(Constants.PERCENTAGE_MOVED, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.MOVED_ROWS, VoltType.BIGINT));
columns.add(new ColumnInfo(Constants.ROWS_PER_SECOND, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.ESTIMATED_REMAINING, VoltType.BIGINT));
columns.add(new ColumnInfo(Constants.MEGABYTES_PER_SECOND, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.CALLS_PER_SECOND, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.CALLS_LATENCY, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.CALLS_TIME, VoltType.FLOAT));
columns.add(new ColumnInfo(Constants.CALLS_TRANSFER_TIME, VoltType.FLOAT));
}
@Override
protected void updateStatsRow(Object rowKey, Object[] rowValues)
{
final StatsPoint point = statsPoint;
rowValues[columnNameToIndex.get(Constants.TIMESTAMP)] = System.currentTimeMillis();
rowValues[columnNameToIndex.get(Constants.PERCENTAGE_MOVED)] = point.getPercentageMoved();
rowValues[columnNameToIndex.get(Constants.MOVED_ROWS)] = point.getMovedRows();
rowValues[columnNameToIndex.get(Constants.ROWS_PER_SECOND)] = point.getRowsPerSecond();
rowValues[columnNameToIndex.get(Constants.ESTIMATED_REMAINING)] = point.getEstimatedRemaining();
rowValues[columnNameToIndex.get(Constants.MEGABYTES_PER_SECOND)] = point.getMegabytesPerSecond();
rowValues[columnNameToIndex.get(Constants.CALLS_PER_SECOND)] = point.getInvocationsPerSecond();
rowValues[columnNameToIndex.get(Constants.CALLS_LATENCY)] = point.getAverageInvocationLatency();
rowValues[columnNameToIndex.get(Constants.CALLS_TIME)] = point.getAverageInvocationTime();
rowValues[columnNameToIndex.get(Constants.CALLS_TRANSFER_TIME)] = point.getAverageInvocationTransferTime();
}
@Override
protected Iterator<Object> getStatsRowKeyIterator(boolean interval)
{
if (totalRangeSize > 0)
{
return Arrays.asList(Object.class.cast(new Long(1))).iterator();
}
else
{
return Collections.emptyList().iterator();
}
}
public static class StatsPoint implements Serializable
{
private static final long serialVersionUID = 2635982992941464809L;
/// Name for logging, etc..
private final String name;
/// Start time in nanoseconds.
private final long startTimeNanos;
/// # of ranges to move.
private final long totalRanges;
/// End time in nanoseconds.
private final long endTimeNanos;
/// # of ranges transferred.
private final long movedRanges;
/// # of rows transferred.
private final long movedRows;
/// # of bytes transferred.
private final long movedBytes;
/// # of calls.
private final long invocationCount;
// Nanoseconds waiting on sysproc call
private final long invocationLatencyNanos;
// Nanoseconds spent executing sysproc
private final long invocationTimeNanos;
// Nanosecond spent transferring data in sysproc
private final long invocationTransferTimeNanos;
/**
* Scratch constructor.
* Default to the current time for start/end. Clear raw statistics.
* @param name stats point name
* @param totalRanges total ranges to move
*/
public StatsPoint(String name, long totalRanges)
{
this(name, null, null, totalRanges, 0, 0, 0, 0, 0, 0, 0);
}
/**
* Full constructor.
* @param name stat point name
* @param startTimeNanos start time in nanoseconds
* @param endTimeNanos end time in nanoseconds
* @param totalRanges total ranges to move
* @param movedRanges moved range count
* @param movedRows moved row count
* @param movedBytes moved byte count
* @param invocationCount invocation count
* @param invocationLatencyNanos time spent waiting on sysproc
*/
public StatsPoint(
String name,
Long startTimeNanos, // can be null
Long endTimeNanos, // can be null
long totalRanges,
long movedRanges,
long movedRows,
long movedBytes,
long invocationCount,
long invocationLatencyNanos,
long invocationTimeNanos,
long invocationTransferTimeNanos)
{
// Substitute the current time for null start or end times
long nowNanos = System.nanoTime();
this.name = name;
this.startTimeNanos = startTimeNanos != null ? startTimeNanos : nowNanos;
this.endTimeNanos = endTimeNanos != null ? endTimeNanos : nowNanos;
this.totalRanges = totalRanges;
this.movedRanges = movedRanges;
this.movedRows = movedRows;
this.movedBytes = movedBytes;
this.invocationCount = invocationCount;
this.invocationLatencyNanos = invocationLatencyNanos;
this.invocationTimeNanos = invocationTimeNanos;
this.invocationTransferTimeNanos = invocationTransferTimeNanos;
}
long getStartTimeMillis()
{
return startTimeNanos / TimeUnit.MILLISECONDS.toNanos(1);
}
long getStartTimeNanos()
{
return startTimeNanos;
}
long getEndTimeMillis()
{
return endTimeNanos / TimeUnit.MILLISECONDS.toNanos(1);
}
long getMovedRows()
{
return movedRows;
}
// Derive duration from start/end times.
long getDurationMillis()
{
return getEndTimeMillis() - getStartTimeMillis();
}
public String getFormattedDuration()
{
return formatTimeInterval(getDurationMillis());
}
long getInvocationCount()
{
return invocationCount;
}
double getInvocationLatencyMillis()
{
return invocationLatencyNanos / (double)TimeUnit.MILLISECONDS.toNanos(1);
}
double getThroughput()
{
return getDurationMillis() == 0 ? 0.0 : movedBytes / getDurationMillis();
}
double getCompletedFraction()
{
return totalRanges == 0 ? 0.0 : (double)movedRanges / totalRanges;
}
public double getPercentageMoved()
{
return totalRanges == 0 ? 0.0 : (movedRanges / (double)totalRanges) * 100.0;
}
public String getFormattedPercentageMovedRate()
{
double nanos = getDurationMillis() * MILLISECONDS.toNanos(1);
return MiscUtils.HumanTime.formatRate(getPercentageMoved(), nanos, "%");
}
public double getRowsPerSecond()
{
final double durationInSecs = getDurationMillis() / 1000.0;
return getDurationMillis() == 0 ? 0.0 : movedRows / durationInSecs;
}
public String getFormattedEstimatedRemaining()
{
return formatTimeInterval(getEstimatedRemaining());
}
public double getEstimatedRemaining() {
double estimatedRemaining = -1.0;
if (movedRanges > 0) {
estimatedRemaining = ((totalRanges * getDurationMillis()) / movedRanges) - getDurationMillis();
}
return estimatedRemaining;
}
public double getRangesPerSecond()
{
final double durationInSecs = getDurationMillis() / 1000.0;
return getDurationMillis() == 0 ? 0.0 : movedRanges / durationInSecs;
}
public double getMegabytesPerSecond()
{
return getDurationMillis() == 0 ?
0.0 : (movedBytes / (1024.0 * 1024.0)) / (getDurationMillis() / 1000.0);
}
public double getInvocationsPerSecond()
{
final double durationInSecs = getDurationMillis() / 1000.0;
return getDurationMillis() == 0 ? 0.0 : invocationCount / durationInSecs;
}
public double getAverageInvocationLatency()
{
//Convert to floating point millis
return invocationCount == 0 ? 0.0 : getInvocationLatencyMillis() / (double)invocationCount;
}
public double getAverageInvocationTime() {
return invocationCount == 0 ? 0.0 : TimeUnit.NANOSECONDS.toMillis(invocationTimeNanos) / (double)invocationCount;
}
public double getAverageInvocationTransferTime() {
return invocationCount == 0 ? 0.0 : TimeUnit.NANOSECONDS.toMillis(invocationTransferTimeNanos) / (double)invocationCount;
}
public final static String formatTimeInterval(double dms)
{
long ldms = (long)dms;
if (ldms < 0) ldms = 0;
final long day = MILLISECONDS.toDays(ldms);
final long hr = MILLISECONDS.toHours( ldms
- DAYS.toMillis(day));
final long min = MILLISECONDS.toMinutes( ldms
- DAYS.toMillis(day)
- HOURS.toMillis(hr));
final long sec = MILLISECONDS.toSeconds( ldms
- DAYS.toMillis(day)
- HOURS.toMillis(hr)
- MINUTES.toMillis(min));
final long ms = MILLISECONDS.toMillis( ldms
- DAYS.toMillis(day)
- HOURS.toMillis(hr)
- MINUTES.toMillis(min)
- SECONDS.toMillis(sec));
return String.format("%d %02d:%02d:%02d.%03d", day, hr, min, sec, ms);
}
/**
* Update statistics.
* @param lastTimeNanos time in nanoseconds
* @param lastInvocationLatencyNanos time spent while invoking the sysproc
* @param lastMovedRanges moved range count
* @param lastMovedRows moved row count
* @param lastMovedBytes moved byte count
* @param lastInvocationCount invocation count
*/
public StatsPoint update(
Long lastTimeNanos,
long lastInvocationLatencyNanos,
long lastInvocationTimeNanos,
long lastInvocationTransferTimeNanos,
long lastMovedRanges,
long lastMovedRows,
long lastMovedBytes,
long lastInvocationCount)
{
return new StatsPoint(
this.name,
this.startTimeNanos,
lastTimeNanos != null ? lastTimeNanos : System.nanoTime(),
this.totalRanges,
this.movedRanges + lastMovedRanges,
this.movedRows + lastMovedRows,
this.movedBytes + lastMovedBytes,
this.invocationCount + lastInvocationCount,
this.invocationLatencyNanos + lastInvocationLatencyNanos,
this.invocationTimeNanos + lastInvocationTimeNanos,
this.invocationTransferTimeNanos + lastInvocationTransferTimeNanos);
}
/**
* Capture a copy of the current stats plus an end time and a recent throughput.
* @param name stats point name
* @param endTimeNanos end time in nanoseconds
* @return immutable snapshot of stats point
*/
public StatsPoint capture(String name, long endTimeNanos)
{
return new StatsPoint(
name,
startTimeNanos,
endTimeNanos,
totalRanges,
movedRanges,
movedRows,
movedBytes,
invocationCount,
invocationLatencyNanos,
invocationTimeNanos,
invocationTransferTimeNanos);
}
@Override
public String toString()
{
return String.format("StatsPoint(%s): "
+ "duration=%.2f s"
+ ", percent=%.2f%% (%s)"
+ ", rows=%d @ %.2f rows/second"
+ ", bytes=%d @ %.2f MB/second"
+ ", invocation=%.2f ms (%d @ %.2f ms latency %.2f ms execution time %.2f ms transfer time)",
name,
getDurationMillis() / 1000.0,
getPercentageMoved(), getFormattedPercentageMovedRate(),
movedRows, getRowsPerSecond(),
movedBytes, getMegabytesPerSecond(),
getInvocationLatencyMillis(), invocationCount,
getAverageInvocationLatency(), getAverageInvocationTime(), getAverageInvocationTransferTime());
}
}
}