
org.sonarsource.performance.measure.DurationMeasureFiles Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-performance-measure Show documentation
Show all versions of sonar-performance-measure Show documentation
Logic to capture a hierarchy of performance measures, save it into a file, and merge files
The newest version!
/*
* SonarSource Performance Measure Library
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program 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 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
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.performance.measure;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class DurationMeasureFiles {
private static final Gson GSON_PRETTY = new GsonBuilder().setPrettyPrinting().create();
private static final DecimalFormatSymbols SYMBOLS = DecimalFormatSymbols.getInstance(Locale.ROOT);
private static final NumberFormat TIME_FORMAT = new DecimalFormat("0.000000000", SYMBOLS);
private static final NumberFormat RANK_FORMAT = new DecimalFormat("000", SYMBOLS);
private DurationMeasureFiles() {
// utility class
}
public static void writeJson(Path path, DurationMeasure measure) throws IOException {
Files.write(path, toJson(measure).getBytes(UTF_8));
}
public static String toJson(DurationMeasure measure) {
String json = GSON_PRETTY.toJson(toJsonObject(measure));
// reduce the number of lines by inlining some of the properties
return json
.replaceAll("\n *+(\"(?:name|calls|durationNanos|children)\":)", " $1")
.replaceAll("(\\d)\n *+\\}", "$1 }");
}
public static JsonObject toJsonObject(DurationMeasure measure) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("name", measure.name());
jsonObject.addProperty("calls", measure.calls());
jsonObject.addProperty("durationNanos", measure.durationNanos());
if (!measure.hasChildren()) {
jsonObject.add("children", measure.sortedChildren().stream()
.map(DurationMeasureFiles::toJsonObject)
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll));
}
return jsonObject;
}
public static DurationMeasure fromJsonWithoutObservationCost(Path performanceFile) throws IOException {
DurationMeasure measure = fromJson(performanceFile);
return MeasurementCost.subtractObservationCost(measure);
}
public static DurationMeasure fromJson(Path performanceFile) throws IOException {
return fromJson(new String(Files.readAllBytes(performanceFile), UTF_8));
}
public static DurationMeasure fromJson(String json) {
JsonObject jsonObject = GSON_PRETTY.fromJson(json, JsonObject.class);
return fromJson(jsonObject);
}
public static DurationMeasure fromJson(JsonObject jsonObject) {
Map childrenMap = null;
JsonArray children = jsonObject.getAsJsonArray("children");
if (children != null && children.size() > 0) {
childrenMap = new HashMap<>();
for (JsonElement child : children) {
DurationMeasure childMeasure = fromJson(child.getAsJsonObject());
childrenMap.merge(childMeasure.name(), childMeasure, DurationMeasure::merge);
}
}
String name = jsonObject.getAsJsonPrimitive("name").getAsString();
long calls = jsonObject.getAsJsonPrimitive("calls").getAsLong();
long durationNanos = jsonObject.getAsJsonPrimitive("durationNanos").getAsLong();
return new DurationMeasure(name, calls, durationNanos, childrenMap);
}
/**
* @param path the destination statistics file
* @param measure the root of the measure hierarchy
* @param categoryNames Map defining categories for grouped measures, when a measure.name() matches
* a key of this map, the value of the map becomes the category of the measure
* and its descendents.
* @param groupedMeasurePredicate Predicate applied on each measure.name() to decide if the measure
* needs to be extracted, grouped and categorised at the end of the
* statistics file.
*/
public static void writeStatistics(Path path, DurationMeasure measure, Map categoryNames,
Predicate groupedMeasurePredicate) throws IOException {
Files.write(path, toStatistics(measure, categoryNames, groupedMeasurePredicate).getBytes(UTF_8));
}
public static String toStatistics(DurationMeasure measure, Map categoryNames, Predicate groupedMeasurePredicate) {
Map> measureCategories = categorizeMeasures(measure, categoryNames, Collections.emptySet());
DurationMeasure measureWithoutGrouped = measure.copy();
measureWithoutGrouped.remove(MeasurementCost.MEASUREMENT_COST_NAME);
measureWithoutGrouped.remove(MeasurementCost.SUBTRACTED_MEASUREMENT_COST_NAME);
Map groupedMeasure = extractGroupedMeasure(measureWithoutGrouped, groupedMeasurePredicate);
StringBuilder stat = new StringBuilder();
stat.append("Performance (in seconds without observation cost)\n");
stat.append(toTextTree(measureWithoutGrouped));
stat.append("\n");
if (!groupedMeasure.isEmpty()) {
stat.append("Grouped Entries (in seconds without observation cost)\n");
stat.append(toRankedList(groupedMeasure, measureCategories));
}
return stat.toString();
}
/**
* Walk recursively through the measure hierarchy and assign a list of category to some measure names.
* If a measure.name() is a key in "categoryNames", then the related value in the "categoryNames" map becomes the measure category.
* The returned map, at the key matching the given measure.name(), will contain its measure category in addition to all
* the measure categories of its parent. The returned map will also contains the returned map of its children.
*/
private static Map> categorizeMeasures(DurationMeasure measure, Map categoryNames, Set parentCategories) {
Map> categories = new HashMap<>();
String newCategory = categoryNames.get(measure.name());
Set measureCategorize = Collections.emptySet();
if (!parentCategories.isEmpty() || newCategory != null) {
measureCategorize = new TreeSet<>();
categories.put(measure.name(), measureCategorize);
measureCategorize.addAll(parentCategories);
if (newCategory != null) {
measureCategorize.add(newCategory);
}
}
for (DurationMeasure child : measure.children()) {
for (Map.Entry> childEntry : categorizeMeasures(child, categoryNames, measureCategorize).entrySet()) {
Set set = categories.computeIfAbsent(childEntry.getKey(), name -> new TreeSet<>());
set.addAll(childEntry.getValue());
if (newCategory != null) {
set.add(newCategory);
}
}
}
if (measureCategorize.isEmpty()) {
categories.remove(measure.name());
}
return categories;
}
private static Map extractGroupedMeasure(DurationMeasure measure, Predicate groupedMeasurePredicate) {
Map groupedMap = new HashMap<>();
measure.children().stream()
.map(child -> extractGroupedMeasure(child, groupedMeasurePredicate))
.forEach(childMap -> childMap.values().forEach(child -> groupedMap.merge(child.name(), child.copy(), DurationMeasure::merge)));
DurationMeasure substitutedChild = new DurationMeasure("[ 0 grouped measure(s) ]");
long groupedCount = 0;
for (DurationMeasure child : new ArrayList<>(measure.children())) {
if (groupedMeasurePredicate.test(child.name())) {
groupedCount++;
measure.remove(child.name());
substitutedChild.addCalls(child.calls(), child.durationNanos());
groupedMap.merge(child.name(), child.copy(), DurationMeasure::merge);
}
}
if (groupedCount > 0) {
substitutedChild.rename("[ " + groupedCount + " grouped measure(s) ]");
measure.addOrMerge(substitutedChild);
}
return groupedMap;
}
public static String toTextTree(DurationMeasure measure) {
StringBuilder out = new StringBuilder();
toTextTreeOrderedByDuration(out, measure, "");
return out.toString();
}
private static void toTextTreeOrderedByDuration(StringBuilder out, DurationMeasure measure, String indent) {
out.append(indent).append("• ").append(toSeconds(measure.durationNanos())).append(" ").append(measure.name()).append("\n");
measure.children().stream()
.sorted(Comparator.comparing(x -> -x.durationNanos()))
.forEach(child -> toTextTreeOrderedByDuration(out, child, indent + " "));
}
private static String toSeconds(long durationNanos) {
double seconds = durationNanos / 1_000_000_000.0d;
return String.format("%13s" , TIME_FORMAT.format(seconds));
}
public static String toRankedList(Map groupedMeasure, Map> measureCategories) {
StringBuilder out = new StringBuilder();
long totalDuration = groupedMeasure.values().stream().mapToLong(DurationMeasure::durationNanos).sum();
out.append("Total ").append(toSeconds(totalDuration)).append("\n");
List checkMeasures = groupedMeasure.values().stream()
.sorted(Comparator.comparing(c -> -c.durationNanos()))
.collect(Collectors.toList());
for (int i = 0; i < checkMeasures.size(); i++) {
DurationMeasure measure = checkMeasures.get(i);
String categoryList = "";
Set categories = measureCategories.get(measure.name());
if (categories != null) {
categoryList = " (" + String.join(", ", categories) + ")";
}
out.append(toRank(i, checkMeasures))
.append(" ").append(toSeconds(measure.durationNanos()))
.append(" ").append(String.format("%-50s", measure.name()))
.append(categoryList)
.append("\n");
measure.children().stream()
.sorted(Comparator.comparing(x -> -x.durationNanos()))
.forEach(child -> toTextTreeOrderedByDuration(out, child, " "));
}
return out.toString();
}
private static String toRank(long index, Collection> collection) {
return RANK_FORMAT.format(index + 1) + "/" + RANK_FORMAT.format(collection.size());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy