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