1/* 2 * Copyright (C) 2013 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.google.caliper.worker; 17 18import static com.google.common.base.Preconditions.checkArgument; 19 20import com.google.caliper.model.Measurement; 21import com.google.caliper.model.Value; 22import com.google.common.base.MoreObjects; 23import com.google.common.base.Objects; 24import com.google.common.collect.ImmutableList; 25import com.google.common.collect.ImmutableMultiset; 26import com.google.common.collect.Multiset; 27import com.google.common.collect.Multiset.Entry; 28import com.google.common.collect.Multisets; 29 30import java.text.DecimalFormat; 31import java.util.Collection; 32 33/** 34 * A set of statistics about the allocations performed by a benchmark method. 35 */ 36final class AllocationStats { 37 private final int allocationCount; 38 private final long allocationSize; 39 private final int reps; 40 private final ImmutableMultiset<Allocation> allocations; 41 42 /** 43 * Constructs a new {@link AllocationStats} with the given number of allocations 44 * ({@code allocationCount}), cumulative size of the allocations ({@code allocationSize}) and the 45 * number of {@code reps} passed to the benchmark method. 46 */ 47 AllocationStats(int allocationCount, long allocationSize, int reps) { 48 this(allocationCount, allocationSize, reps, ImmutableMultiset.<Allocation>of()); 49 } 50 51 /** 52 * Constructs a new {@link AllocationStats} with the given allocations and the number of 53 * {@code reps} passed to the benchmark method. 54 */ 55 AllocationStats(Collection<Allocation> allocations, int reps) { 56 this(allocations.size(), Allocation.getTotalSize(allocations), reps, 57 ImmutableMultiset.copyOf(allocations)); 58 } 59 60 private AllocationStats(int allocationCount, long allocationSize, int reps, 61 Multiset<Allocation> allocations) { 62 checkArgument(allocationCount >= 0, "allocationCount (%s) was negative", allocationCount); 63 this.allocationCount = allocationCount; 64 checkArgument(allocationSize >= 0, "allocationSize (%s) was negative", allocationSize); 65 this.allocationSize = allocationSize; 66 checkArgument(reps >= 0, "reps (%s) was negative", reps); 67 this.reps = reps; 68 this.allocations = Multisets.copyHighestCountFirst(allocations); 69 } 70 71 int getAllocationCount() { 72 return allocationCount; 73 } 74 75 long getAllocationSize() { 76 return allocationSize; 77 } 78 79 /** 80 * Computes and returns the difference between this measurement and the given 81 * {@code baseline} measurement. The {@code baseline} measurement must have a lower weight 82 * (fewer reps) than this measurement. 83 */ 84 AllocationStats minus(AllocationStats baseline) { 85 for (Entry<Allocation> entry : baseline.allocations.entrySet()) { 86 int superCount = allocations.count(entry.getElement()); 87 if (superCount < entry.getCount()) { 88 throw new IllegalStateException( 89 String.format("Your benchmark appears to have non-deterministic allocation behavior. " 90 + "Observed %d instance(s) of %s in the baseline but only %d in the actual " 91 + "measurement", 92 entry.getCount(), 93 entry.getElement(), 94 superCount)); 95 } 96 } 97 try { 98 return new AllocationStats(allocationCount - baseline.allocationCount, 99 allocationSize - baseline.allocationSize, 100 reps - baseline.reps, 101 Multisets.difference(allocations, baseline.allocations)); 102 } catch (IllegalArgumentException e) { 103 throw new IllegalStateException(String.format( 104 "Your benchmark appears to have non-deterministic allocation behavior. The difference " 105 + "between the baseline %s and the measurement %s is invalid. Consider enabling " 106 + "instrument.allocation.options.trackAllocations to get a more specific error message.", 107 baseline, this), e); 108 } 109 } 110 111 /** 112 * Computes and returns the difference between this measurement and the given 113 * {@code baseline} measurement. Unlike {@link #minus(AllocationStats)} this does not have to 114 * be a super set of the baseline. 115 */ 116 public Delta delta(AllocationStats baseline) { 117 return new Delta( 118 allocationCount - baseline.allocationCount, 119 allocationSize - baseline.allocationSize, 120 reps - baseline.reps, 121 Multisets.difference(allocations, baseline.allocations), 122 Multisets.difference(baseline.allocations, allocations)); 123 } 124 125 /** 126 * Returns a list of {@link Measurement measurements} based on this collection of stats. 127 */ 128 ImmutableList<Measurement> toMeasurements() { 129 for (Entry<Allocation> entry : allocations.entrySet()) { 130 double allocsPerRep = ((double) entry.getCount()) / reps; 131 System.out.printf("Allocated %f allocs per rep of %s%n", allocsPerRep, entry.getElement()); 132 } 133 return ImmutableList.of( 134 new Measurement.Builder() 135 .value(Value.create(allocationCount, "")) 136 .description("objects") 137 .weight(reps) 138 .build(), 139 new Measurement.Builder() 140 .value(Value.create(allocationSize, "B")) 141 .weight(reps) 142 .description("bytes") 143 .build()); 144 } 145 146 @Override 147 public boolean equals(Object obj) { 148 if (obj == this) { 149 return true; 150 } else if (obj instanceof AllocationStats) { 151 AllocationStats that = (AllocationStats) obj; 152 return allocationCount == that.allocationCount 153 && allocationSize == that.allocationSize 154 && reps == that.reps 155 && Objects.equal(allocations, that.allocations); 156 } else { 157 return false; 158 } 159 } 160 161 @Override 162 public int hashCode() { 163 return Objects.hashCode(allocationCount, allocationSize, reps, allocations); 164 } 165 166 @Override public String toString() { 167 return MoreObjects.toStringHelper(this) 168 .add("allocationCount", allocationCount) 169 .add("allocationSize", allocationSize) 170 .add("reps", reps) 171 .add("allocations", allocations) 172 .toString(); 173 } 174 175 /** 176 * The delta between two different sets of statistics. 177 */ 178 static final class Delta { 179 private final int count; 180 private final long size; 181 private final int reps; 182 private final Multiset<Allocation> additions; 183 private final Multiset<Allocation> removals; 184 185 Delta( 186 int count, 187 long size, 188 int reps, 189 Multiset<Allocation> additions, 190 Multiset<Allocation> removals) { 191 this.count = count; 192 this.size = size; 193 this.reps = reps; 194 this.additions = additions; 195 this.removals = removals; 196 } 197 198 /** 199 * Returns the long formatted with a leading +/- sign 200 */ 201 private static String formatWithLeadingSign(long n) { 202 return n > 0 ? "+" + n : "" + n; 203 } 204 205 @Override public String toString() { 206 return MoreObjects.toStringHelper(this) 207 .add("count", formatWithLeadingSign(count)) 208 .add("size", formatWithLeadingSign(size)) 209 .add("reps", formatWithLeadingSign(reps)) 210 .add("additions", additions) 211 .add("removals", removals) 212 .toString(); 213 } 214 } 215} 216