/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sirona.aop;
import org.apache.sirona.Role;
import org.apache.sirona.SironaException;
import org.apache.sirona.configuration.Configuration;
import org.apache.sirona.counters.Counter;
import org.apache.sirona.repositories.Repository;
import org.apache.sirona.stopwatches.StopWatch;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A method interceptor that compute method invocation performances.
* <p/>
* Concrete implementation will adapt the method interception API to
* this class requirement.
*
*/
public abstract class AbstractPerformanceInterceptor<T> implements Serializable {
// static for performances reasons, all these values are read through getXXX so it is overridable
private static final boolean ADAPTIVE = Configuration.is(Configuration.CONFIG_PROPERTY_PREFIX + "performance.adaptive", false);
private static final long FORCED_ITERATION = Configuration.getInteger(Configuration.CONFIG_PROPERTY_PREFIX + "performance.forced-iteration", 0);
private static final long THRESHOLD = duration(Configuration.getProperty(Configuration.CONFIG_PROPERTY_PREFIX + "performance.threshold", null));
private static final ActivationContext ALWAYS_ACTIVE_CONTEXT = new ActivationContext(true, 0, 0);
protected static final ConcurrentMap<Object, ActivationContext> CONTEXTS = new ConcurrentHashMap<Object, ActivationContext>();
private static long duration(final String duration) {
if (duration == null) {
return 0;
}
final String[] parts = duration.split(" ");
if (parts.length == 1) {
return Long.parseLong(duration.trim());
} else if (parts.length == 2) {
return TimeUnit.valueOf(parts[2].trim().toUpperCase(Locale.ENGLISH)).toNanos(Long.parseLong(parts[0].trim()));
}
return 0;
}
protected MonitorNameExtractor monitorNameExtractor;
public AbstractPerformanceInterceptor() {
setMonitorNameExtractor(DefaultMonitorNameExtractor.INSTANCE);
}
/**
* API neutral method invocation
*/
protected Object doInvoke(final T invocation) throws Throwable {
final String name = getCounterName(invocation);
if (name == null) {
return proceed(invocation);
}
final Context ctx = before(invocation, name);
Throwable error = null;
try {
return proceed(invocation);
} catch (final Throwable t) {
error = t;
throw t;
} finally {
if (error == null) {
ctx.stop();
} else {
ctx.stopWithException(error);
}
}
}
protected Context before(final T invocation, final String name) {
final ActivationContext context = doFindContext(invocation);
try
{
final StopWatch stopwatch;
if (context.shouldExecute()) {
Repository repository = Repository.INSTANCE;
if (repository==null){
System.out.println("repository is null");
}
final Counter monitor = repository.getCounter(getKey(invocation, name));
if ( monitor == null){
System.out.println("monitor is null");
}
stopwatch = Repository.INSTANCE.start(monitor);
} else {
stopwatch = null;
}
return newContext(invocation, context, stopwatch);
}
catch ( Exception e )
{
//e.printStackTrace();
// ignore and return a fake context can happen on start when intercepting some classes
// and all agent classes not really loaded
return newContext( invocation, context, new StopWatch()
{
@Override
public long getElapsedTime()
{
return 0;
}
@Override
public StopWatch stop()
{
return this;
}
} );
}
}
protected Context newContext(final T invocation, final ActivationContext context, final StopWatch stopwatch) {
return new Context(context, stopwatch);
}
protected Counter.Key getKey(final T invocation, final String name) {
return new Counter.Key(getRole(), name);
}
protected boolean isAdaptive() {
return ADAPTIVE;
}
protected Object extractContextKey(final T invocation) {
return null;
}
protected ActivationContext getOrCreateContext(final Object m) {
final ActivationContext c = CONTEXTS.get(m);
if (c == null) {
final String counterName;
if (SerializableMethod.class.isInstance(m)) {
counterName = getCounterName(null, SerializableMethod.class.cast(m).method());
} else {
counterName = m.toString();
}
return putAndGetActivationContext(m, new ActivationContext(true, counterName));
}
return c;
}
protected ActivationContext putAndGetActivationContext(Object m, ActivationContext newCtx) {
final ActivationContext old = CONTEXTS.putIfAbsent(m, newCtx);
if (old != null) {
newCtx = old;
}
return newCtx;
}
protected ActivationContext doFindContext(final T invocation) {
if (!isAdaptive()) {
return ALWAYS_ACTIVE_CONTEXT;
}
final Object m = extractContextKey(invocation);
if (m != null) {
return getOrCreateContext(m);
}
return ALWAYS_ACTIVE_CONTEXT;
}
protected Role getRole() {
return Role.PERFORMANCES;
}
protected abstract Object proceed(T invocation) throws Throwable;
protected abstract String getCounterName(T invocation);
/**
* Compute the counter name associated to this method invocation
*
* @param method method being invoked
* @return counter name. If <code>null</code>, nothing will be monitored
*/
protected String getCounterName(final Object instance, final Method method) {
return monitorNameExtractor.getMonitorName(instance, method);
}
public void setMonitorNameExtractor(final MonitorNameExtractor monitorNameExtractor) {
this.monitorNameExtractor = monitorNameExtractor;
}
/**
* The handler for cases where interception is not possible and you need to pass the "before"object to be able to monitor.
*/
public static class Context {
private static final int MAX_LENGTH = Configuration.getInteger(Configuration.CONFIG_PROPERTY_PREFIX + "performance.exception.max-length", 100);
protected final ActivationContext activationContext;
protected final StopWatch stopWatch;
public Context(final ActivationContext activationContext, final StopWatch stopWatch) {
this.activationContext = activationContext;
this.stopWatch = stopWatch;
}
public void stop() {
if (stopWatch != null) {
final long elapsedTime = stopWatch.stop().getElapsedTime();
activationContext.elapsedTime(elapsedTime);
}
}
public void stopWithException(final Throwable error) {
if (stopWatch != null) {
stopWatch.stop();
final long elapsedTime = stopWatch.getElapsedTime();
if (error != null) {
/*
final ByteArrayOutputStream writer = new ByteArrayOutputStream();
error.printStackTrace(new PrintStream(writer));
final String toString = writer.toString();
*/
Repository.INSTANCE.getCounter(
new Counter.Key(Role.FAILURES, error.getClass().getName() + ":" + (error.getMessage() != null ? error.getMessage() : ""))).add(elapsedTime);
}
activationContext.elapsedTime(elapsedTime);
}
}
}
protected static class SerializableMethod implements Serializable {
protected final String clazz;
protected final String method;
protected transient Method realMethod;
protected final int hashCode;
public SerializableMethod(final String clazz, final String method, final Method reflectMethod) {
this.clazz = clazz;
this.method = method;
this.realMethod = reflectMethod;
this.hashCode = reflectMethod.hashCode();
}
public SerializableMethod(final Method m) {
this(m.getDeclaringClass().getName(), m.getName(), m);
}
public Method method() {
if (realMethod == null) { // try to find it
try {
Class<?> declaring = Thread.currentThread().getContextClassLoader().loadClass(clazz);
while (declaring != null) {
for (final Method m : declaring.getDeclaredMethods()) {
if (m.getName().equals(method)) {
realMethod = m;
return realMethod;
}
}
declaring = declaring.getSuperclass();
}
} catch (final ClassNotFoundException e) {
throw new SironaException(e.getMessage(),e);
}
}
return realMethod;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final SerializableMethod that = SerializableMethod.class.cast(o);
if (method != null && that.method != null) {
return method.equals(that.method);
}
return hashCode == that.hashCode;
}
@Override
public int hashCode() {
return hashCode;
}
}
/**
* This class contains the activation/deactivation logic.
*/
protected static class ActivationContext implements Serializable {
protected final long forceIteration;
protected final long threshold;
protected final boolean thresholdActive;
protected volatile boolean active = true;
protected volatile AtomicInteger iteration = new AtomicInteger(0);
public ActivationContext(final boolean active, final long th, final long it) {
this.active = active;
if (it >= 0) {
forceIteration = it;
} else {
forceIteration = FORCED_ITERATION;
}
if (th >= 0) {
threshold = th;
} else {
threshold = THRESHOLD;
}
this.thresholdActive = this.threshold > 0;
}
public ActivationContext(final boolean active, final String name) {
this(active,
duration(Configuration.getProperty(Configuration.CONFIG_PROPERTY_PREFIX + "performance." + name + ".threshold", null)),
Configuration.getInteger(Configuration.CONFIG_PROPERTY_PREFIX + "performance." + name + ".forced-iteration", -1));
}
public boolean isForcedIteration() {
return iteration.incrementAndGet() > forceIteration;
}
protected long getThreshold() {
return threshold;
}
protected boolean isThresholdActive() {
return thresholdActive;
}
public boolean isActive() {
return active;
}
public void reset() {
active = false;
iteration.set(0);
}
public boolean shouldExecute() {
return isActive() || isForcedIteration();
}
public void elapsedTime(final long elapsedTime) {
if (isThresholdActive() && elapsedTime < getThreshold()) {
reset();
}
}
}
}