/*
* Copyright 2015-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.event;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.immutables.value.Value;
/**
* An implementation of {@link BuckEvent}s used for gathering performance statistics. These are only
* intended to be used with the trace viewer and should not be used to communicate information
* between parts of the system.
*/
public abstract class SimplePerfEvent extends AbstractBuckEvent {
public SimplePerfEvent(EventKey eventKey) {
super(eventKey);
}
public enum Type {
STARTED("Started"),
UPDATED("Updated"),
FINISHED("Finished"),
;
private String value;
public String getValue() {
return value;
}
Type(String value) {
this.value = value;
}
}
/** @return event identifier. */
public abstract PerfEventId getEventId();
/** @return event type. */
public abstract Type getEventType();
/** @return information associated with the event. */
public abstract ImmutableMap<String, Object> getEventInfo();
/**
* Prefer using {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)} when
* possible.
*
* <p>Create an event that indicates the start of a particular operation.
*
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @param info Any additional information to be saved with the event. This will be serialized and
* potentially sent over the wire, so please keep this small.
* @return an object that should be used to create the corresponding update and finished event.
*/
public static Started started(PerfEventId perfEventId, ImmutableMap<String, Object> info) {
return new StartedImpl(perfEventId, info);
}
/**
* Convenience wrapper around {@link SimplePerfEvent#started(PerfEventId, ImmutableMap)}.
*
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @return an object that should be used to create the corresponding update and finished event.
*/
public static Started started(PerfEventId perfEventId) {
return started(perfEventId, ImmutableMap.of());
}
/**
* Convenience wrapper around {@link SimplePerfEvent#started(PerfEventId, ImmutableMap)}.
*
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @param k1 name of the value to be stored with the event.
* @param v1 value to be stored. This will be serialized and potentially sent over the wire, so
* please keep this small.
* @return an object that should be used to create the corresponding update and finished event.
*/
public static Started started(PerfEventId perfEventId, String k1, Object v1) {
return started(perfEventId, ImmutableMap.of(k1, v1));
}
/**
* Convenience wrapper around {@link SimplePerfEvent#started(PerfEventId, ImmutableMap)}.
*
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @param k1 name of the value to be stored with the event.
* @param v1 value to be stored. This will be serialized and potentially sent over the wire, so
* please keep this small.
* @param k2 name of the value to be stored with the event.
* @param v2 value to be stored. This will be serialized and potentially sent over the wire, so
* please keep this small.
* @return an object that should be used to create the corresponding update and finished event.
*/
public static Started started(
PerfEventId perfEventId, String k1, Object v1, String k2, Object v2) {
return started(perfEventId, ImmutableMap.of(k1, v1, k2, v2));
}
/**
* Creates a scope within which the measured operation takes place.
*
* <pre>
* try (perfEvent = SimplePerfEvent.scope(bus, PerfEventId.of("BurnCpu"))) {
* int bitsFlopped = 0;
* for (int i = 0; i < 1000; i++) {
* bitsFlopped += invokeExpensiveOp();
* if (bitsFlopped % 100 == 0) {
* // Some of these events are of special interest.
* perfEvent.update("noFlopIteration", i);
* }
* }
* perfEvent.appendFinishedInfo("totalBitsFlopped", bitsFlopped);
* }
* </pre>
*
* @param bus the {@link BuckEventBus} to post update and finished events to.
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @param info Any additional information to be saved with the event. This will be serialized and
* potentially sent over the wire, so please keep this small.
* @return an AutoCloseable which will send the finished event as soon as control flow exits the
* scope.
*/
public static Scope scope(
BuckEventBus bus, PerfEventId perfEventId, ImmutableMap<String, Object> info) {
StartedImpl started = new StartedImpl(perfEventId, info);
bus.post(started);
return new SimplePerfEventScope(bus, started);
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(BuckEventBus bus, PerfEventId perfEventId) {
return scope(bus, perfEventId, ImmutableMap.of());
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(BuckEventBus bus, PerfEventId perfEventId, String k1, Object v1) {
return scope(bus, perfEventId, ImmutableMap.of(k1, v1));
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(
BuckEventBus bus, PerfEventId perfEventId, String k1, Object v1, String k2, Object v2) {
return scope(bus, perfEventId, ImmutableMap.of(k1, v1, k2, v2));
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(
Optional<BuckEventBus> bus, PerfEventId perfEventId, ImmutableMap<String, Object> info) {
if (bus.isPresent()) {
return scope(bus.get(), perfEventId, info);
} else {
return new NoopScope();
}
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(Optional<BuckEventBus> bus, PerfEventId perfEventId) {
return scope(bus, perfEventId, ImmutableMap.of());
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(
Optional<BuckEventBus> bus, PerfEventId perfEventId, String k, Object v) {
return scope(bus, perfEventId, ImmutableMap.of(k, v));
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(
Optional<BuckEventBus> bus,
PerfEventId perfEventId,
String k,
Object v,
String k2,
Object v2) {
return scope(bus, perfEventId, ImmutableMap.of(k, v, k2, v2));
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scope(BuckEventBus bus, String perfEventName) {
return scope(bus, PerfEventId.of(perfEventName), ImmutableMap.<String, Object>of());
}
/**
* Like {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}, but doesn't post
* the events if the duration of the scope is below a certain threshold. NOTE: The events are
* buffered before being posted. The longer they are buffered, the more likely any logging code
* will be confused. Ideally the threshold should not exceed 100ms.
*
* @param bus the {@link BuckEventBus} to post update and finished events to.
* @param perfEventId identifier of the operation (Upload, CacheFetch, Parse, etc..).
* @param info Any additional information to be saved with the event. This will be serialized and
* potentially sent over the wire, so please keep this small.
* @param minimumTime the scope must take at least this long for the event to be posted.
* @param timeUnit time unit for minimumTime.
* @return an AutoCloseable which will send the finished event as soon as control flow exits the
* scope.
*/
public static Scope scopeIgnoringShortEvents(
BuckEventBus bus,
PerfEventId perfEventId,
ImmutableMap<String, Object> info,
Scope parentScope,
long minimumTime,
TimeUnit timeUnit) {
if (minimumTime == 0) {
return scope(bus, perfEventId, info);
}
StartedImpl started = new StartedImpl(perfEventId, info);
bus.timestamp(started);
return new MinimumTimePerfEventScope(bus, started, parentScope, timeUnit.toNanos(minimumTime));
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scopeIgnoringShortEvents(
BuckEventBus bus,
PerfEventId perfEventId,
Scope parentScope,
long minimumTime,
TimeUnit timeUnit) {
return scopeIgnoringShortEvents(
bus, perfEventId, ImmutableMap.of(), parentScope, minimumTime, timeUnit);
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scopeIgnoringShortEvents(
BuckEventBus bus,
PerfEventId perfEventId,
String k1,
Object v1,
Scope parentScope,
long minimumTime,
TimeUnit timeUnit) {
return scopeIgnoringShortEvents(
bus, perfEventId, ImmutableMap.of(k1, v1), parentScope, minimumTime, timeUnit);
}
/**
* Convenience wrapper for {@link SimplePerfEvent#scope(BuckEventBus, PerfEventId, ImmutableMap)}.
*/
public static Scope scopeIgnoringShortEvents(
BuckEventBus bus,
PerfEventId perfEventId,
String k1,
Object v1,
String k2,
Object v2,
Scope parentScope,
long minimumTime,
TimeUnit timeUnit) {
return scopeIgnoringShortEvents(
bus, perfEventId, ImmutableMap.of(k1, v1, k2, v2), parentScope, minimumTime, timeUnit);
}
/** Represents the scope within which a particular performance operation is taking place. */
public interface Scope extends AutoCloseable {
/**
* Creates and sends an event which indicates an update in state of the scope. Every invocation
* creates and sends a new event.
*
* @param info Any additional information to be saved with the event. This will be serialized
* and potentially sent over the wire, so please keep this small.
*/
void update(ImmutableMap<String, Object> info);
/** Convenience wrapper for {@link Scope#update(ImmutableMap)}. */
void update(String k1, Object v1);
/** Convenience wrapper for {@link Scope#update(ImmutableMap)}. */
void update(String k1, Object v1, String k2, Object v2);
/**
* Appends information to the finished event which will be sent when control exits the scope.
*
* @param k1 name of the value to be stored with the event.
* @param v1 value to be stored. This will be serialized and potentially sent over the wire, so
* please keep this small.
*/
void appendFinishedInfo(String k1, Object v1);
/** Increments a counter that will be appended to the finished event. */
void incrementFinishedCounter(String key, long increment);
@Override
void close();
}
public interface Started extends BuckEvent {
/**
* Creates a new event which indicates an update to the performance data being gathered.
*
* @param info Any additional information to be saved with the event. This will be serialized
* and potentially sent over the wire, so please keep this small.
* @return An event which should be posted to the {@link BuckEventBus}.
*/
BuckEvent createUpdateEvent(ImmutableMap<String, Object> info);
/** Convenience wrapper for {@link Started#createUpdateEvent(String, Object)}. */
BuckEvent createUpdateEvent(String k1, Object v1);
/** Convenience wrapper for {@link Started#createUpdateEvent(String, Object)}. */
BuckEvent createUpdateEvent(String k1, Object v1, String k2, Object v2);
/**
* Creates a new event which indicates the end of a performance event.
*
* @param info Any additional information to be saved with the event. This will be serialized
* and potentially sent over the wire, so please keep this small.
* @return An event which should be posted to the {@link BuckEventBus}.
*/
BuckEvent createFinishedEvent(ImmutableMap<String, Object> info);
/** Convenience wrapper for {@link Started#createFinishedEvent(String, Object)}. */
BuckEvent createFinishedEvent();
/** Convenience wrapper for {@link Started#createFinishedEvent(String, Object)}. */
BuckEvent createFinishedEvent(String k1, Object v1);
/** Convenience wrapper for {@link Started#createFinishedEvent(String, Object)}. */
BuckEvent createFinishedEvent(String k1, Object v1, String k2, Object v2);
}
@Value.Immutable
@BuckStyleImmutable
/**
* This is an identifier for the various performance event names in use in the system. Should be
* CamelCase (first letter capitalized).
*/
abstract static class AbstractPerfEventId {
@JsonValue
@Value.Parameter
public abstract String getValue();
@Value.Check
protected void nameIsNotEmpty() {
Preconditions.checkArgument(!getValue().isEmpty());
}
}
private static class NoopScope implements AutoCloseable, Scope {
@Override
public void update(ImmutableMap<String, Object> info) {}
@Override
public void update(String k1, Object v1) {}
@Override
public void update(String k1, Object v1, String k2, Object v2) {}
@Override
public void appendFinishedInfo(String k1, Object v1) {}
@Override
public void incrementFinishedCounter(String key, long increment) {}
@Override
public void close() {}
}
private static class SimplePerfEventScope implements AutoCloseable, Scope {
protected BuckEventBus bus;
protected StartedImpl started;
protected ConcurrentMap<String, AtomicLong> finishedCounters;
protected ImmutableMap.Builder<String, Object> finishedInfoBuilder;
public SimplePerfEventScope(BuckEventBus bus, StartedImpl started) {
this.bus = bus;
this.started = started;
this.finishedCounters = new ConcurrentHashMap<>();
this.finishedInfoBuilder = ImmutableMap.builder();
}
@Override
public void update(ImmutableMap<String, Object> info) {
bus.post(new Updated(started, info));
}
@Override
public void update(String k1, Object v1) {
update(ImmutableMap.of(k1, v1));
}
@Override
public void update(String k1, Object v1, String k2, Object v2) {
update(ImmutableMap.of(k1, v1, k2, v2));
}
@Override
public void appendFinishedInfo(String key, Object value) {
finishedInfoBuilder.put(key, value);
}
@Override
public void incrementFinishedCounter(String key, long delta) {
AtomicLong count = finishedCounters.get(key);
if (count == null) {
finishedCounters.putIfAbsent(key, new AtomicLong(0));
count = finishedCounters.get(key);
}
count.addAndGet(delta);
}
protected Finished createFinishedEvent() {
finishedInfoBuilder.putAll(Maps.transformValues(finishedCounters, AtomicLong::get));
return new Finished(started, finishedInfoBuilder.build());
}
@Override
public void close() {
bus.post(createFinishedEvent());
}
}
private static class MinimumTimePerfEventScope extends SimplePerfEventScope {
private final long minimumDurationNanos;
private final Scope parentScope;
private final List<AbstractChainablePerfEvent> events;
public MinimumTimePerfEventScope(
BuckEventBus bus, StartedImpl started, Scope parentScope, long minimumDurationNanos) {
super(bus, started);
this.minimumDurationNanos = minimumDurationNanos;
this.parentScope = parentScope;
this.events = new ArrayList<>();
this.events.add(started);
}
@Override
public void update(ImmutableMap<String, Object> info) {
Updated event = new Updated(started, info);
bus.timestamp(event);
events.add(event);
}
@Override
public void close() {
Finished finished = createFinishedEvent();
bus.timestamp(finished);
events.add(finished);
long delta = finished.getNanoTime() - started.getNanoTime();
if (delta >= minimumDurationNanos) {
for (AbstractChainablePerfEvent event : events) {
bus.postWithoutConfiguring(event);
}
} else {
parentScope.incrementFinishedCounter(
started.getEventId().getValue() + "_accumulated_duration_ns", delta);
parentScope.incrementFinishedCounter(
started.getEventId().getValue() + "_accumulated_count", 1);
}
}
}
/** Common implementation for the Started/Updated/Finished events. */
private abstract static class AbstractChainablePerfEvent extends SimplePerfEvent {
private PerfEventId perfEventId;
private Type perfEventType;
private ImmutableMap<String, Object> info;
public AbstractChainablePerfEvent(
EventKey eventKey,
PerfEventId perfEventId,
Type perfEventType,
ImmutableMap<String, Object> info) {
super(eventKey);
this.perfEventId = perfEventId;
this.perfEventType = perfEventType;
this.info = info;
}
@Override
public PerfEventId getEventId() {
return perfEventId;
}
@Override
public Type getEventType() {
return perfEventType;
}
@Override
public ImmutableMap<String, Object> getEventInfo() {
return info;
}
@Override
protected String getValueString() {
return Joiner.on(',').withKeyValueSeparator(":").join(info);
}
@Override
public String getEventName() {
return new StringBuilder("PerfEvent")
.append(perfEventId.getValue())
.append(perfEventType.getValue())
.toString();
}
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), perfEventId, perfEventType, info);
}
@Override
public boolean equals(Object o) {
if (!super.equals(o)) {
return false;
}
AbstractChainablePerfEvent other = (AbstractChainablePerfEvent) o;
return other.perfEventId.equals(perfEventId)
&& other.info.equals(info)
&& other.perfEventType.equals(perfEventType);
}
}
private static class StartedImpl extends AbstractChainablePerfEvent implements Started {
private boolean isChainFinished = false;
/** @return whether creating new events off of this chain is allowed. */
@JsonIgnore
public boolean isChainFinished() {
return isChainFinished;
}
/** Called to indicate that the chain should not allow any further events. */
public void markChainFinished() {
isChainFinished = true;
}
@Override
public BuckEvent createUpdateEvent(ImmutableMap<String, Object> info) {
Preconditions.checkState(!isChainFinished());
return new Updated(this, info);
}
@Override
public BuckEvent createUpdateEvent(String k1, Object v1) {
Preconditions.checkState(!isChainFinished());
return createUpdateEvent(ImmutableMap.of(k1, v1));
}
@Override
public BuckEvent createUpdateEvent(String k1, Object v1, String k2, Object v2) {
Preconditions.checkState(!isChainFinished());
return createUpdateEvent(ImmutableMap.of(k1, v1, k2, v2));
}
@Override
public BuckEvent createFinishedEvent(ImmutableMap<String, Object> info) {
Preconditions.checkState(!isChainFinished());
return new Finished(this, info);
}
@Override
public BuckEvent createFinishedEvent() {
Preconditions.checkState(!isChainFinished());
return createFinishedEvent(ImmutableMap.of());
}
@Override
public BuckEvent createFinishedEvent(String k1, Object v1) {
Preconditions.checkState(!isChainFinished());
return createFinishedEvent(ImmutableMap.of(k1, v1));
}
@Override
public BuckEvent createFinishedEvent(String k1, Object v1, String k2, Object v2) {
Preconditions.checkState(!isChainFinished());
return createFinishedEvent(ImmutableMap.of(k1, v1, k2, v2));
}
public StartedImpl(PerfEventId perfEventId, ImmutableMap<String, Object> info) {
super(EventKey.unique(), perfEventId, Type.STARTED, info);
}
}
private static class Updated extends AbstractChainablePerfEvent {
public Updated(StartedImpl started, ImmutableMap<String, Object> updateInfo) {
super(started.getEventKey(), started.getEventId(), Type.UPDATED, updateInfo);
}
}
private static class Finished extends AbstractChainablePerfEvent {
public Finished(StartedImpl started, ImmutableMap<String, Object> finishedInfo) {
super(started.getEventKey(), started.getEventId(), Type.FINISHED, finishedInfo);
started.markChainFinished();
}
}
}