package net.tuis.ubench;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.tuis.ubench.scale.MathEquation;
/**
* Factory class and reporting instances that allow functions to be tested for
* scalability.
*
* @author rolf
* @author Simon Forsberg
*
*/
public class UScale {
private static final Logger LOGGER = UUtils.getLogger(UScale.class);
private static final int SCALE_LIMIT = 12_000_000;
@FunctionalInterface
private interface TaskRunnerBuilder {
TaskRunner build(String name, int scale);
}
private final List<UStats> stats;
private final String title;
private UScale(String title, List<UStats> stats) {
this.title = title;
this.stats = stats;
}
/**
* Get the name this UScale report was titled with.
* @return the title used when created
*/
public String getTitle() {
return title;
}
/**
* Generate and print (System.out) the scalability report.
*/
public void report() {
try (Writer w = new NonClosingSystemOut()) {
report(w);
} catch (IOException e) {
throw new IllegalStateException("Should never be an exception writing to System.out", e);
}
}
/**
* Generate and print (System.out) the scalability report.
* @param writer The writer to write the report to
* @throws IOException in the event that the writer throws one.
*/
public void report(Writer writer) throws IOException {
String report = stats.stream()
.sorted(Comparator.comparingInt(UStats::getIndex))
.map(sr -> String.format(
"Scale %4d -> %8d (count %d, threshold %d)",
sr.getIndex(), sr.getAverageRawNanos(), sr.getCount(), UUtils.getNanoTick()))
.collect(Collectors.joining("\n"));
MathEquation bestFit = determineBestFit();
writer.write(title);
char[] uls = new char[title.length()];
Arrays.fill(uls, '=');
writer.write(uls);
writer.write(report);
writer.write("Best fit is: " + bestFit + "\n");
writer.flush();
}
/**
* Retrieve the best-fitting possible standard scaling equation that describes these scaling results.
* @return the best fit equation
*/
public MathEquation determineBestFit() {
return ScaleDetect.detect(this);
}
/**
* Retrieve the possible standard scaling equations that describes these scaling results in best-fit first order.
* @return the standard scaling equations in best-fit-first order.
*/
public MathEquation[] fitEquations() {
return ScaleDetect.rank(this);
}
/**
* Get the data as JSON data in an array format (<code>[ [scale,nanos], ...]</code>
* @return a JSON formatted string containing the raw data.
*/
public String toJSONString() {
String rawdata = stats.stream().sorted(Comparator.comparingInt(UStats::getIndex))
.map(sr -> sr.toJSONString())
.collect(Collectors.joining(",\n ", "[\n ", "\n ]"));
String fields = Stream.of(UStats.getJSONFields()).collect(Collectors.joining("\", \"", "[\"", "\"]"));
String models = Stream.of(ScaleDetect.rank(this)).map(me -> me.toJSONString()).collect(Collectors.joining(",\n ", "[\n ", "\n ]"));
return String.format("{ title: \"%s\",\n nano_tick: %d,\n models: %s,\n fields: %s,\n data: %s\n}",
title, UUtils.getNanoTick(), models, fields, rawdata);
}
/**
* Create an HTML document (with data and chart) plotting the performance.
* @param target the destination to store the HTML document at.
* @throws IOException if there is a problem writing to the target path
*/
public void reportHTML(final Path target) throws IOException {
Files.createDirectories(target.toAbsolutePath().getParent());
LOGGER.info(() -> "Preparing HTML Report '" + title + "' to path: " + target);
String html = UUtils.readResource("net/tuis/ubench/scale/UScale.html");
Map<String, String> subs = new HashMap<>();
subs.put("DATA", toJSONString());
subs.put("TITLE", title);
for (Map.Entry<String, String> me : subs.entrySet()) {
html = html.replaceAll(Pattern.quote("${" + me.getKey() + "}"), Matcher.quoteReplacement(me.getValue()));
}
Files.write(target, html.getBytes(StandardCharsets.UTF_8));
LOGGER.info(() -> "Completed HTML Report '" + title + "' to path: " + target);
}
/**
* Test the scalability of a consumer that requires T input data.
* <p>
* This method calls <code>scale(Consumer, IntFunction, boolean)</code> with
* the reusedata parameter set to true:
*
* <pre>
* return scale(function, scaler, true);
* </pre>
*
* This means that the data will be generated once for each scale factor,
* and reused.
*
* @param <T>
* the type of the input data needed by the Function
* @param title
* the title to apply to all reports and results
* @param function
* the Function that computes the T data
* @param scaler
* a supplier that can supply T data of different sizes
* @return A UScale instance containing the results of the testing
*/
public static <T> UScale function(String title, Function<T, ?> function, IntFunction<T> scaler) {
return function(title, function, scaler, true);
}
/**
* Test the scalability of a consumer that requires T input data.
*
* @param <T>
* the type of the input data needed by the Function
* @param title
* the title to apply to all reports and results
* @param function
* the computer that processes the T data
* @param scaler
* a supplier that can supply T data of different sizes
* @param reusedata
* if true, data of each size will be created just once, and
* reused often.
* @return A UScale instance containing the results of the testing
*/
public static <T> UScale function(String title, Function<T, ?> function, IntFunction<T> scaler, final boolean reusedata) {
return consumer(title, function::apply, scaler, reusedata);
}
/**
* Test the scalability of a consumer that requires T input data.
* <p>
* This method calls <code>scale(Consumer, IntFunction, boolean)</code> with
* the reusedata parameter set to true:
*
* <pre>
* return scale(function, scaler, true);
* </pre>
*
* This means that the data will be generated once for each scale factor,
* and reused.
*
* @param <T>
* the type of the input data needed by the Consumer
* @param title
* the title to apply to all reports and results
* @param consumer
* the Consumer that processes the T data
* @param scaler
* a supplier that can supply T data of different sizes
* @return A UScale instance containing the results of the testing
*/
public static <T> UScale consumer(String title, Consumer<T> consumer, IntFunction<T> scaler) {
return consumer(title, consumer, scaler, true);
}
/**
* Test the scalability of a consumer that requires T input data.
*
* @param <T>
* the type of the input data needed by the Consumer
* @param title
* the title to apply to all reports and results
* @param consumer
* the Consumer that processes the T data
* @param scaler
* a supplier that can supply T data of different sizes
* @param reusedata
* if true, data of each size will be created just once, and
* reused often.
* @return A UScale instance containing the results of the testing
*/
public static <T> UScale consumer(String title, Consumer<T> consumer, IntFunction<T> scaler, final boolean reusedata) {
final ScaleControl<T> scontrol = new ScaleControl<>(consumer, scaler, reusedata);
final TaskRunnerBuilder builder = (name, scale) -> scontrol.buildTask(name, scale);
return scaleMapper(title, builder);
}
private static final UScale scaleMapper(final String title, final TaskRunnerBuilder scaleBuilder) {
LOGGER.info(title + ": Starting Scalability testing");
final long start = System.nanoTime();
LOGGER.finer("warming up task");
UStats[] rep = UMode.PARALLEL.getModel().executeTasks("Warmup", scaleBuilder.build("warmup", 2));
LOGGER.fine(() -> "Warmed up results:\n" + rep[0].toString());
final List<UStats> results = new ArrayList<>(20);
for (int i = 1; i <= SCALE_LIMIT; i *= 2) {
results.add(runStats(title, i, scaleBuilder));
if (results.get(results.size() -1).getCount() <= 3) {
break;
}
}
if (results.size() > 4) {
final int last = results.get(results.size() - 1).getIndex();
int step = last >> 3;
for (int j = last - step; j > step; j -= step) {
if (j == last >> 1 || j == last >> 2) {
continue;
}
results.add(runStats(title, j, scaleBuilder));
}
}
LOGGER.info(String.format("%s: Completed tests in %.3fms", title, (System.nanoTime() - start) / 1000000.0));
return new UScale(title, results);
}
private static UStats runStats(String title, int i, TaskRunnerBuilder scaleBuilder) {
final String runName = title + ": Scale " + i;
final long beg = System.nanoTime();
final TaskRunner runner = scaleBuilder.build(runName, i);
LOGGER.finer(() -> String.format("Built data for %s in %.3fms", runName, (System.nanoTime() - beg) / 1000000.0));
UStats stats = UMode.SEQUENTIAL.getModel().executeTasks(runName, runner)[0];
if (LOGGER.isLoggable(Level.INFO)) {
final long time = System.nanoTime() - beg;
LOGGER.fine(() -> String.format("Completed scale test %s in %.3fms", runName, time / 1000000.0));
}
return stats;
}
/**
* Obtain a copy of the statistics produced for this UScale instance.
* @return a copy of the underlying statistics.
*/
public List<UStats> getStats() {
return new ArrayList<>(stats);
}
}