├── AUTHORS ├── .DS_Store ├── src ├── .DS_Store ├── main │ └── java │ │ └── com │ │ └── google │ │ └── monitoring │ │ ├── runtime │ │ └── instrumentation │ │ │ ├── .DS_Store │ │ │ ├── sample │ │ │ ├── SampleStrategy.java │ │ │ ├── PeriodicSampler.java │ │ │ └── AllocationCountSampler.java │ │ │ ├── recorders │ │ │ ├── Recorder.java │ │ │ ├── LifetimeRecorder.java │ │ │ ├── FlamePrinter.java │ │ │ ├── FlameRecorder.java │ │ │ └── LifetimePrinter.java │ │ │ ├── events │ │ │ ├── Event.java │ │ │ ├── AllocationEvent.java │ │ │ ├── LifetimeEvent.java │ │ │ └── EventParser.java │ │ │ ├── InstrumentationProperties.java │ │ │ ├── adapters │ │ │ ├── EscapeAnalyzer.java │ │ │ ├── AllocationClassAdapter.java │ │ │ ├── VerifyingClassAdapter.java │ │ │ ├── EscapeMethodAdapter.java │ │ │ └── AllocationMethodAdapter.java │ │ │ ├── Test.java │ │ │ ├── ObjectSizeMeasurement.java │ │ │ ├── InstrumentationPropertiesImpl.java │ │ │ ├── AllocationRecorder.java │ │ │ ├── StaticClassWriter.java │ │ │ └── AllocationInstrumenter.java │ │ └── flame │ │ ├── StackTraceParsers.java │ │ └── FlameCollapse.java └── test │ └── java │ └── com │ └── google │ └── monitoring │ └── runtime │ └── instrumentation │ └── sample │ └── SamplerTest.java ├── examples ├── .DS_Store ├── 1201_1414.png ├── group_by_protobuf.png └── highlighted_protobuf.png ├── .gitmodules ├── .gitignore ├── generate.sh ├── flame.properties ├── flame-gen.sh ├── README.md ├── pom.xml └── COPYING /AUTHORS: -------------------------------------------------------------------------------- 1 | Google Inc. 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/examples/.DS_Store -------------------------------------------------------------------------------- /examples/1201_1414.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/examples/1201_1414.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "FlameGraph"] 2 | path = FlameGraph 3 | url = https://github.com/brendangregg/FlameGraph.git 4 | -------------------------------------------------------------------------------- /examples/group_by_protobuf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/examples/group_by_protobuf.png -------------------------------------------------------------------------------- /examples/highlighted_protobuf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/examples/highlighted_protobuf.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.ims 3 | *.iml 4 | 5 | .classpath 6 | .project 7 | .settings/ 8 | 9 | target/ 10 | 11 | bin/ 12 | out/ 13 | 14 | stacks.txt 15 | collapsed.txt 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmaloney10/allocation-instrumenter/HEAD/src/main/java/com/google/monitoring/runtime/instrumentation/.DS_Store -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/sample/SampleStrategy.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.sample; 2 | 3 | /** 4 | * Created by jmaloney on 11/29/16. 5 | */ 6 | public interface SampleStrategy { 7 | boolean canSample(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/recorders/Recorder.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.recorders; 2 | 3 | /** 4 | * Created by jmaloney on 10/10/16. 5 | */ 6 | public interface Recorder { 7 | void record(int count, String desc, Object newObj); 8 | } 9 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | GROUP_BY="" 4 | OUTPUT_FILE=$(date +%m%d_%H%M) 5 | INPUT="stacks.txt" 6 | 7 | while getopts "g:f:i:" opt; do 8 | case "$opt" in 9 | g) 10 | GROUP_BY=${OPTARG} 11 | ;; 12 | i) 13 | INPUT=${OPTARG} 14 | ;; 15 | f) 16 | OUTPUT_FILE=${OPTARG} 17 | ;; 18 | esac 19 | done 20 | 21 | 22 | java -jar target/java-allocation-instrumenter-3.0-SNAPSHOT.jar ${INPUT} ${GROUP_BY} 23 | FlameGraph/flamegraph.pl collapsed.txt > ${OUTPUT_FILE}.html 24 | -------------------------------------------------------------------------------- /flame.properties: -------------------------------------------------------------------------------- 1 | ##### General properties 2 | # Use flame or lifetime recorder 3 | recorder=flame 4 | # Sample output file 5 | output.file=/tmp/stacks.txt 6 | # methodName,methodClassName,methodClassLineNumber (default methodClassName) 7 | stack.trace.verbosity=methodClassName 8 | # Whether or not to estimate object size 9 | record.size=true 10 | 11 | ##### Sampling properties 12 | # Delay sampling during startup 13 | sample.delay.secs=30 14 | # Either time/allocationCount 15 | sample.strategy=time 16 | # for time sample.strategy 17 | sample.interval.ms=100 18 | # for allocationCount sample.strategy 19 | sample.rate=100000 -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/events/Event.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.events; 2 | 3 | /** 4 | * Created by jmaloney on 11/29/16. 5 | */ 6 | public interface Event { 7 | 8 | /** 9 | * Get the size in bytes of the allocation 10 | * 11 | * @return the size in bytes of the object 12 | */ 13 | long getSize(); 14 | 15 | /** 16 | * Get the class name of the object allocated 17 | * 18 | * @return the name of the object sampled 19 | */ 20 | String getObjectName(); 21 | 22 | /** 23 | * Get the stacktrace associated with the sample 24 | * 25 | * @return an array of StackTraceElements one for each stack frame 26 | */ 27 | StackTraceElement[] getTrace(); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/events/AllocationEvent.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.events; 2 | 3 | /** 4 | * Created during object allocation. It contains data from the sampled allocation that is passed from the allocating 5 | * thread to the FlamePrinter thread where the data is processed and recorded. 6 | * 7 | * Created by jmaloney on 5/23/2016. 8 | */ 9 | public class AllocationEvent implements Event { 10 | private final long size; 11 | private final StackTraceElement[] trace; 12 | private final String objName; 13 | 14 | public AllocationEvent(final long size, 15 | final StackTraceElement[] trace, 16 | final String objName){ 17 | this.size = size; 18 | this.trace = trace; 19 | this.objName = objName; 20 | } 21 | 22 | @Override 23 | public long getSize() { 24 | return size; 25 | } 26 | 27 | @Override 28 | public StackTraceElement[] getTrace() { 29 | return trace; 30 | } 31 | 32 | @Override 33 | public String getObjectName() { 34 | return objName; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /flame-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TMPDIR=./tmp_flame 4 | 5 | rm -rf $TMPDIR 2> /dev/null 6 | mkdir $TMPDIR 2> /dev/null 7 | 8 | trap ctrl_c INT 9 | profiling="" 10 | 11 | function ctrl_c() { 12 | if [ "$profiling" != "" ] 13 | then 14 | sh ./flame-svg-gen.sh $TMPTRACE 15 | fi 16 | exit 17 | } 18 | 19 | 20 | TMPTRACE=./tmp_flame/trace.txt 21 | rm $TMPTRACE 2> /dev/null 22 | pid="" 23 | if [ "$1" == "" ] 24 | then 25 | echo "waiting for java process..." 26 | while [ "$pid" == "" ] 27 | do 28 | #For Windows with Cygwin use this 29 | #pid=`ps -W | grep java | grep -o [0-9]* | head -1` 30 | pid=`ps | grep java | grep -v grep | grep -o [0-9]* | head -1` 31 | if [ "$pid" != "" ] 32 | then 33 | break 34 | fi 35 | done 36 | else 37 | pid=$1; 38 | fi 39 | #For Windows with Cygwin use this 40 | #processExists=`ps -pW $pid | grep $pid` 41 | processExists=`ps -p $pid | grep $pid` 42 | if [ "$processExists" != "" ] 43 | then 44 | echo "process found pid: $pid" 45 | echo "profiling started, press ctrl+c to end and generate report" 46 | profiling="true" 47 | while true; 48 | do 49 | jstack $pid >> $TMPTRACE && sleep 0.7; 50 | done 51 | else 52 | echo "process with id $pid doesn't exists" 53 | fi 54 | 55 | #echo "Done! please check Flame.svg" 56 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/events/LifetimeEvent.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.events; 2 | 3 | import java.lang.ref.WeakReference; 4 | 5 | /** 6 | * Created during object allocation. It contains data from the sampled allocation that is passed from the allocating 7 | * thread to the LifetimePrinter thread where the data is processed and recorded. 8 | * 9 | * Created by jmaloney on 10/10/16. 10 | */ 11 | public class LifetimeEvent implements Event { 12 | private final WeakReference object; 13 | private final String objectName; 14 | private final StackTraceElement[] trace; 15 | private final long size; 16 | 17 | public LifetimeEvent(final WeakReference reference, 18 | final String objectName, 19 | final StackTraceElement[] trace, 20 | final long size){ 21 | this.object = reference; 22 | this.objectName = objectName; 23 | this.trace = trace; 24 | this.size = size; 25 | } 26 | 27 | public boolean alive(){ 28 | return object.get() != null; 29 | } 30 | 31 | @Override 32 | public String getObjectName(){ 33 | return objectName; 34 | } 35 | 36 | @Override 37 | public StackTraceElement[] getTrace() { 38 | return trace; 39 | } 40 | 41 | @Override 42 | public long getSize() { 43 | return size; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/InstrumentationProperties.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation; 2 | 3 | import com.google.monitoring.runtime.instrumentation.events.EventParser; 4 | 5 | /** 6 | * Created by jmaloney on 12/1/16. 7 | */ 8 | public interface InstrumentationProperties { 9 | 10 | // PROPERTY NAMES 11 | String RECORDER_PROPERTY = "recorder"; 12 | String RECORD_SIZE_PROPERTY = "record.size"; 13 | String OUTPUT_PATH_PROPERTY = "output.file"; 14 | String VERBOSITY_LEVEL_PROPERTY = "stack.trace.verbosity"; 15 | 16 | String SAMPLE_STRATEGY_PROPERTY = "sample.strategy"; 17 | String DELAY_SECS_PROPERTY = "sample.delay.secs"; 18 | String SAMPLE_RATE_PROPERTY = "sample.rate"; 19 | String SAMPLE_INTERVAL_PROPERTY = "sample.interval.ms"; 20 | 21 | 22 | // DEFAULTS 23 | String DEFAULT_RECORDER = "flame"; 24 | boolean DEFAULT_RECORD_SIZE = true; 25 | String DEFAULT_OUTPUT_PATH = "/tmp/stacks.txt"; 26 | EventParser.VerbosityLevel DEFAULT_VERBOSITY_LEVEL = EventParser.VerbosityLevel.METHOD_CLASS_NAME; 27 | 28 | String DEFAULT_SAMPLE_STRATEGY = "allocationCount"; 29 | long DEFAULT_DELAY_SECS = 0L; 30 | long DEFAULT_SAMPLE_RATE = 10_000L; 31 | long DEFAULT_SAMPLE_INTERVAL = 10L; 32 | 33 | 34 | // ACCESS METHODS 35 | String recorder(); 36 | boolean recordSize(); 37 | String outputPath(); 38 | EventParser.VerbosityLevel verbosityLevel(); 39 | 40 | String sampleStrategy(); 41 | long delaySecs(); 42 | long sampleRate(); 43 | long sampleInterval(); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/sample/PeriodicSampler.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.sample; 2 | 3 | import java.util.concurrent.ThreadLocalRandom; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | 6 | /** 7 | * Samples periodically. An atomic nextSampleTime keeps track of when the earliest time the next 8 | * sample can be taken. When the current time is greater than this the next sample time is calculated 9 | * and a CAS with the nextSampleTime is attempted, if successful canSample() returns true. 10 | * 11 | * Created by jmaloney on 11/29/16. 12 | */ 13 | public class PeriodicSampler implements SampleStrategy { 14 | private final AtomicLong nextSampleTime = new AtomicLong(); 15 | private final long minSampleInterval; 16 | private final long jitter; 17 | 18 | public PeriodicSampler(final long start, final long minSampleInterval, final long jitter){ 19 | this.nextSampleTime.set(start); 20 | this.minSampleInterval = minSampleInterval; 21 | this.jitter = jitter; 22 | } 23 | 24 | @Override 25 | public boolean canSample() { 26 | final long now = System.currentTimeMillis(); 27 | final long nextTime = nextSampleTime.get(); 28 | if (now > nextTime){ 29 | return nextSampleTime.compareAndSet(nextTime, now + minSampleInterval + getJitter()); 30 | } else { 31 | return false; 32 | } 33 | } 34 | 35 | /** 36 | * Based on the amount of jitter set select a random value between [-1 * jitter, jitter) 37 | * 38 | * @return 39 | */ 40 | private long getJitter(){ 41 | if (jitter == 0){ 42 | return 0; 43 | } else { 44 | return ThreadLocalRandom.current().nextLong(-1 * jitter, jitter); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/google/monitoring/runtime/instrumentation/sample/SamplerTest.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.sample; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Created by jmaloney on 12/1/16. 8 | */ 9 | public class SamplerTest { 10 | 11 | @Test 12 | public void allocationCountBasicTest(){ 13 | final long pastTime = System.currentTimeMillis() - 10; 14 | 15 | SampleStrategy sampleStrategy = new AllocationCountSampler(pastTime, 1, 0); 16 | for(int i = 0; i < 100; i++){ 17 | Assert.assertTrue(sampleStrategy.canSample()); 18 | } 19 | 20 | sampleStrategy = new AllocationCountSampler(pastTime, 2, 0); 21 | Assert.assertTrue(sampleStrategy.canSample()); 22 | Assert.assertFalse(sampleStrategy.canSample()); 23 | Assert.assertTrue(sampleStrategy.canSample()); 24 | Assert.assertFalse(sampleStrategy.canSample()); 25 | } 26 | 27 | @Test 28 | public void allocationCountNotStartedTest() throws InterruptedException { 29 | final long futureTime = System.currentTimeMillis() + 10; 30 | SampleStrategy sampleStrategy = new AllocationCountSampler(futureTime, 1, 0); 31 | Assert.assertFalse(sampleStrategy.canSample()); 32 | } 33 | 34 | @Test 35 | public void periodicBasicTest() throws InterruptedException { 36 | final long pastTime = System.currentTimeMillis() - 10; 37 | SampleStrategy sampleStrategy = new PeriodicSampler(pastTime, 0, 0); 38 | for(int i = 0; i < 100; i++){ 39 | Thread.sleep(1); 40 | Assert.assertTrue(sampleStrategy.canSample()); 41 | } 42 | } 43 | 44 | @Test 45 | public void periodicNotStartedTest() throws InterruptedException { 46 | final long futureTime = System.currentTimeMillis() + 10; 47 | SampleStrategy sampleStrategy = new PeriodicSampler(futureTime, 1, 0); 48 | Assert.assertFalse(sampleStrategy.canSample()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/recorders/LifetimeRecorder.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.recorders; 2 | 3 | import com.google.monitoring.runtime.instrumentation.AllocationRecorder; 4 | import com.google.monitoring.runtime.instrumentation.ObjectSizeMeasurement; 5 | import com.google.monitoring.runtime.instrumentation.events.LifetimeEvent; 6 | 7 | import java.lang.instrument.Instrumentation; 8 | import java.lang.ref.WeakReference; 9 | import java.util.concurrent.BlockingQueue; 10 | 11 | /** 12 | * Created by jmaloney on 10/10/16. 13 | */ 14 | public class LifetimeRecorder implements Recorder { 15 | 16 | private final BlockingQueue queue; 17 | private final long id; 18 | private final boolean recordSize; 19 | 20 | public LifetimeRecorder(final BlockingQueue queue, 21 | final long id, 22 | final boolean recordSize){ 23 | this.queue = queue; 24 | this.id = id; 25 | this.recordSize = recordSize; 26 | } 27 | 28 | @Override 29 | public void record(final int count, final String desc, final Object newObj) { 30 | if (Thread.currentThread().getId() != id) { 31 | final long objectSize; 32 | final Instrumentation instr; 33 | if (recordSize && (instr = AllocationRecorder.getInstrumentation()) != null) { 34 | // Copy value into local variable to prevent NPE that occurs when 35 | // instrumentation field is set to null by this class's shutdown hook 36 | // after another thread passed the null check but has yet to call 37 | // instrumentation.getObjectSize() 38 | objectSize = ObjectSizeMeasurement.getObjectSize(newObj, (count >= 0), instr); 39 | } else { 40 | objectSize = 1; 41 | } 42 | 43 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 44 | LifetimeEvent lifetimeEvent = new LifetimeEvent(new WeakReference<>(newObj), desc, trace, objectSize); 45 | queue.offer(lifetimeEvent); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/recorders/FlamePrinter.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.recorders; 2 | 3 | import com.google.monitoring.runtime.instrumentation.InstrumentationProperties; 4 | import com.google.monitoring.runtime.instrumentation.events.AllocationEvent; 5 | import com.google.monitoring.runtime.instrumentation.events.EventParser; 6 | 7 | import java.io.*; 8 | import java.util.Properties; 9 | import java.util.concurrent.BlockingQueue; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * Pulls AllocationEvent's off of the queue and outputs the data in the desired form. 14 | * 15 | * Created by jmaloney on 5/23/2016. 16 | */ 17 | public class FlamePrinter extends Thread { 18 | private BlockingQueue queue; 19 | private final Writer writer; 20 | private final EventParser.VerbosityLevel verbosityLevel; 21 | 22 | public FlamePrinter(final InstrumentationProperties properties) throws FileNotFoundException, UnsupportedEncodingException { 23 | writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(properties.outputPath()), "utf-8")); 24 | verbosityLevel = properties.verbosityLevel(); 25 | } 26 | 27 | public void close() throws IOException { 28 | writer.close(); 29 | } 30 | 31 | private void process(final AllocationEvent event){ 32 | final String entry = EventParser.parseEvent(event, verbosityLevel); 33 | if (!entry.equals("")) { 34 | try { 35 | writer.write(entry); 36 | } catch (IOException e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | } 41 | 42 | public void setQueue(final BlockingQueue queue){ 43 | this.queue = queue; 44 | } 45 | 46 | @Override 47 | public void run() { 48 | Thread.currentThread().setName("FlamePrinter"); 49 | for(;;){ 50 | try { 51 | final AllocationEvent event = queue.poll(1, TimeUnit.MILLISECONDS); 52 | if (event != null) { 53 | process(event); 54 | } 55 | } catch (InterruptedException e) { 56 | e.printStackTrace(); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/recorders/FlameRecorder.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.recorders; 2 | 3 | import com.google.monitoring.runtime.instrumentation.AllocationRecorder; 4 | import com.google.monitoring.runtime.instrumentation.ObjectSizeMeasurement; 5 | import com.google.monitoring.runtime.instrumentation.events.AllocationEvent; 6 | 7 | import java.lang.instrument.Instrumentation; 8 | import java.util.concurrent.BlockingQueue; 9 | 10 | /** 11 | * Samples memory allocations. Each invocation makes an AllocationEvent which is put onto a queue to be processed on a 12 | * separate thread. The sampleAllocation method is executed on the same thread where the actual allocation occurs. 13 | * 14 | * Created by jmaloney on 5/23/2016. 15 | */ 16 | public class FlameRecorder implements Recorder { 17 | 18 | private final BlockingQueue queue; 19 | private final long id; 20 | private final boolean recordSize; 21 | 22 | public FlameRecorder(final BlockingQueue queue, 23 | final long id, 24 | final boolean recordSize){ 25 | this.queue = queue; 26 | this.id = id; 27 | this.recordSize = recordSize; 28 | } 29 | 30 | @Override 31 | public void record(final int count, final String desc, final Object newObj){ 32 | if (Thread.currentThread().getId() != id) { 33 | final long objectSize; 34 | final Instrumentation instr; 35 | if (recordSize && (instr = AllocationRecorder.getInstrumentation()) != null) { 36 | // Copy value into local variable to prevent NPE that occurs when 37 | // instrumentation field is set to null by this class's shutdown hook 38 | // after another thread passed the null check but has yet to call 39 | // instrumentation.getObjectSize() 40 | objectSize = ObjectSizeMeasurement.getObjectSize(newObj, (count >= 0), instr); 41 | } else { 42 | objectSize = 1; 43 | } 44 | 45 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 46 | AllocationEvent allocationEvent = new AllocationEvent(objectSize, trace, desc); 47 | queue.offer(allocationEvent); 48 | } 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/adapters/EscapeAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.adapters; 2 | 3 | import com.sun.management.ThreadMXBean; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | /** 9 | * Created by jmaloney on 6/27/2016. 10 | */ 11 | public class EscapeAnalyzer { 12 | private static ConcurrentHashMap skipMap = new ConcurrentHashMap<>(); 13 | 14 | private static class ByteAllocationTracker { 15 | private static final int MAX_DEPTH = 128; 16 | private final ThreadMXBean bean = (ThreadMXBean) ManagementFactory.getThreadMXBean(); 17 | private final long[] thisThread = new long[1]; 18 | private long[][] start = new long[MAX_DEPTH][2]; 19 | private int depth = 0; 20 | private long invocationCount = 0; 21 | 22 | public ByteAllocationTracker(){ 23 | thisThread[0] = Thread.currentThread().getId(); 24 | } 25 | 26 | public void start(){ 27 | if (depth < MAX_DEPTH) { 28 | start[depth][0] = bean.getThreadAllocatedBytes(thisThread)[0]; 29 | start[depth][1] = invocationCount; 30 | } else { 31 | System.err.println("Warning exceed maximum depth"); 32 | } 33 | invocationCount++; 34 | depth++; 35 | } 36 | 37 | public void stop(String methodName){ 38 | depth--; 39 | if (skipMap.get(methodName) == null) { 40 | long stop = bean.getThreadAllocatedBytes(thisThread)[0]; 41 | if (depth < MAX_DEPTH) { 42 | if (stop - start[depth][0] == 24 * (invocationCount - start[depth][1])){ 43 | skipMap.put(methodName, true); 44 | System.out.println(methodName); 45 | } 46 | } else { 47 | System.err.println("Warning exceed maximum depth: " + methodName); 48 | } 49 | invocationCount++; 50 | } 51 | } 52 | } 53 | 54 | private static ThreadLocal threadLocalByteAllocationTracker = new ThreadLocal(){ 55 | @Override 56 | protected ByteAllocationTracker initialValue(){ 57 | return new ByteAllocationTracker(); 58 | } 59 | }; 60 | 61 | public static void start(){ 62 | threadLocalByteAllocationTracker.get().start(); 63 | } 64 | 65 | public static void stop(String methodName){ 66 | threadLocalByteAllocationTracker.get().stop(methodName); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/sample/AllocationCountSampler.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.sample; 2 | 3 | import java.util.concurrent.ThreadLocalRandom; 4 | 5 | /** 6 | * A thread local allocation count sampling strategy. This strategy keeps thread local counts of 7 | * allocations and compares that the the thread local sample rate. If the sample rate is the same 8 | * as the count canSample() returns true, resets the local count, and calculates the next sampleRate. 9 | * 10 | * Created by jmaloney on 11/29/16. 11 | */ 12 | public class AllocationCountSampler implements SampleStrategy { 13 | 14 | private final ThreadLocal threadLocalSampleCnt = new ThreadLocal() { 15 | @Override 16 | protected Long initialValue() { 17 | return 0L; 18 | } 19 | }; 20 | private final ThreadLocal threadLocalSampleRate = new ThreadLocal() { 21 | @Override 22 | protected Long initialValue() { 23 | return -1L; 24 | } 25 | }; 26 | 27 | private volatile boolean started = false; 28 | private final long startTime; 29 | private final long jitter; 30 | private final long desiredSampleRate; 31 | 32 | public AllocationCountSampler(final long start, final long desiredSampleRate, final long jitter){ 33 | this.startTime = start; 34 | this.jitter = jitter; 35 | this.desiredSampleRate = desiredSampleRate; 36 | } 37 | 38 | @Override 39 | public boolean canSample() { 40 | final long currentSampleRate = threadLocalSampleRate.get(); 41 | final long currentSampleCnt = threadLocalSampleCnt.get() + 1; 42 | 43 | if (currentSampleRate == -1 && !started && System.currentTimeMillis() > startTime){ 44 | started = true; 45 | threadLocalSampleCnt.set(0L); 46 | threadLocalSampleRate.set(desiredSampleRate + getJitter()); 47 | return true; 48 | } else if (currentSampleRate == currentSampleCnt){ 49 | threadLocalSampleCnt.set(0L); 50 | threadLocalSampleRate.set(desiredSampleRate + getJitter()); 51 | return true; 52 | } else { 53 | threadLocalSampleCnt.set(currentSampleCnt); 54 | return false; 55 | } 56 | } 57 | 58 | /** 59 | * Based on the amount of jitter set select a random value between [-1 * jitter, jitter) 60 | * 61 | * @return 62 | */ 63 | private long getJitter(){ 64 | if (jitter == 0){ 65 | return 0; 66 | } else { 67 | return ThreadLocalRandom.current().nextLong(-1 * jitter, jitter); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/events/EventParser.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.events; 2 | 3 | 4 | /** 5 | * Created by jmaloney on 11/29/16. 6 | */ 7 | public class EventParser { 8 | public enum VerbosityLevel{ 9 | METHOD_NAME, 10 | METHOD_CLASS_NAME, 11 | METHOD_CLASS_LINE_NUMBER; 12 | } 13 | 14 | /** 15 | * Events should be parsed to the following format depending on verbosityLevel: 16 | * METHOD_NAME - methodName1;methodName2;methodName3 1253 17 | * METHOD_CLASS_NAME - classPathAndName1.methodName1;classPathAndName2.methodName2 1253 18 | * METHOD_CLASS_LINE_NUMBER - classPathAndName1.methodName1:1;classPathAndName2.methodName2:2 1253 19 | * 20 | * where 1253 is the items size in bytes 21 | * 22 | * @param event the event to parse 23 | * @param verbosityLevel the verbosityLevel to parse the event into 24 | * @return a string representation of the event 25 | */ 26 | public static String parseEvent(final Event event, final VerbosityLevel verbosityLevel){ 27 | final StringBuilder builder = new StringBuilder(); 28 | for (int i = event.getTrace().length - 1; i >= 0 ; i--){ 29 | final StackTraceElement stackTraceElement = event.getTrace()[i]; 30 | if (stackTraceElement.getClassName() == null || stackTraceElement.getMethodName() == null) { 31 | return ""; 32 | } else if (stackTraceElement.getClassName().startsWith("com.google.monitoring.runtime.instrumentation.")){ 33 | //truncate the stack trace once it gets into instrumentation 34 | break; 35 | } 36 | 37 | switch (verbosityLevel){ 38 | case METHOD_NAME: 39 | builder.append(stackTraceElement.getMethodName()); 40 | break; 41 | case METHOD_CLASS_NAME: 42 | builder.append(stackTraceElement.getClassName()) 43 | .append(".") 44 | .append(stackTraceElement.getMethodName()); 45 | break; 46 | case METHOD_CLASS_LINE_NUMBER: 47 | builder.append(stackTraceElement.getClassName()) 48 | .append(".") 49 | .append(stackTraceElement.getMethodName()) 50 | .append(":") 51 | .append(stackTraceElement.getLineNumber()); 52 | break; 53 | } 54 | 55 | builder.append(";"); 56 | } 57 | builder.append(event.getObjectName()).append(" ").append(event.getSize()).append("\n"); 58 | return builder.toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/Test.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation; 2 | 3 | import com.google.monitoring.runtime.instrumentation.adapters.EscapeAnalyzer; 4 | import com.sun.management.ThreadMXBean; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.util.HashMap; 8 | 9 | /** 10 | * Created by jmaloney on 5/23/2016. 11 | */ 12 | public class Test { 13 | 14 | public static void main(String [] args) { 15 | // System.out.println("start"); 16 | // long start = System.currentTimeMillis(); 17 | // HashMap bigMap = allocateMap(); 18 | // Integer count = 0; 19 | // for (Integer i = 0; i < 100000000; i++){ 20 | // String hello = new String("foo") + i; 21 | // bigMap.put(i,hello); 22 | // Integer length = hello.length(); 23 | // count = count + length + i; 24 | // String str = allocateString(); 25 | // Long val = allocateLong(); 26 | // HashMap map = allocateMap(); 27 | // } 28 | // 29 | // System.out.println("count " + count); 30 | // System.out.println("Elapsed: " + (System.currentTimeMillis() - start)); 31 | 32 | //-javaagent:target/java-allocation-instrumenter-3.0-SNAPSHOT.jar=flame.properties 33 | ThreadMXBean bean = (ThreadMXBean) ManagementFactory.getThreadMXBean(); 34 | long[] thisThread = new long[1]; 35 | thisThread[0] = Thread.currentThread().getId(); 36 | long total = 10000000; 37 | long start = bean.getThreadAllocatedBytes(thisThread)[0]; 38 | for (int i = 0; i < total; i++){ 39 | called(); 40 | } 41 | long end = bean.getThreadAllocatedBytes(thisThread)[0]; 42 | System.out.println((end - start)/total); 43 | System.out.println(var); 44 | } 45 | 46 | static long var = 0; 47 | static void called(){ 48 | 49 | Long val = new Long(5); 50 | int ip = 111111111; 51 | EscapeAnalyzer.start(); 52 | String st = ((ip >> 24) & 0xff)+ "." + 53 | ((ip >> 16) & 0xff) + "." + 54 | ((ip >> 8) & 0xff) + "." + 55 | (ip & 0xff); 56 | EscapeAnalyzer.stop("add"); 57 | 58 | var += val + (st == null ? 0 : 1); 59 | 60 | } 61 | 62 | /* 63 | * arrays: 64 | * int/byte/char/boolean/float 64 65 | * long/double 48 66 | * object/(int[64][] but not int[1][1]) 64 67 | * 68 | */ 69 | static String allocateString(){ 70 | return new String("dumbffffff123"); 71 | } 72 | 73 | static Long allocateLong(){ 74 | return new Long(124); 75 | } 76 | 77 | static HashMap allocateMap(){ return new HashMap();} 78 | 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/flame/StackTraceParsers.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.flame; 2 | 3 | /** 4 | * Created by jmaloney on 8/23/16. 5 | */ 6 | class StackTraceParsers { 7 | 8 | /** 9 | * Truncates stack frames prior to the filter keyword and squashes recursion. 10 | * Stack traces that don't contain the filter keyword will be left intact. 11 | * 12 | * @param key key 13 | * @param filter filter 14 | * @return 15 | */ 16 | public static String squashTruncateNoFilter(final String key, final String filter){ 17 | if (!key.toLowerCase().contains(filter)){ 18 | return squash(key); 19 | } 20 | return truncateAndSquash(key, filter); 21 | } 22 | 23 | /** 24 | * Truncates stack frames prior to the filter keyword and squashes recursion. 25 | * Stack traces that don't contain the filter keyword will be removed. 26 | * 27 | * @param key key 28 | * @param filter filter 29 | * @return 30 | */ 31 | public static String squashTruncateFilter(final String key, final String filter){ 32 | if (!key.toLowerCase().contains(filter)){ 33 | return ""; 34 | } 35 | return truncateAndSquash(key, filter); 36 | } 37 | 38 | private static String truncateAndSquash(final String key, final String filter){ 39 | final String[] stackFrames = key.split(";"); 40 | final StringBuilder builder = new StringBuilder(); 41 | boolean startPrinting = false; 42 | 43 | for(int i = 0; i < stackFrames.length; i++){ 44 | if (stackFrames[i].toLowerCase().contains(filter)){ 45 | startPrinting = true; 46 | } 47 | if (startPrinting) { 48 | if (i != 0 && stackFrames[i].equals(stackFrames[i - 1])) { 49 | continue; 50 | } else { 51 | builder.append(stackFrames[i]).append(";"); 52 | } 53 | } 54 | } 55 | return builder.toString(); 56 | } 57 | 58 | // /** 59 | // * Squashes recursion into a single frame. This only works on flat recursion (a 60 | // * method calling itself e.g. a() -> a() -> a().... => a()) if there is some sort 61 | // * of trampolining it will not be collapsed (e.g. a() -> b() -> a() -> b().... 62 | // * will remain the same). 63 | // * 64 | // * @param key key 65 | // * @return 66 | // */ 67 | public static String squash(final String key) { 68 | final String[] stackFrames = key.split(";"); 69 | final StringBuilder builder = new StringBuilder(); 70 | 71 | for(int i = 0; i < stackFrames.length; i++){ 72 | if (i != 0 && stackFrames[i].equals(stackFrames[i - 1])) { 73 | continue; 74 | } else { 75 | builder.append(stackFrames[i]).append(";"); 76 | } 77 | } 78 | return builder.toString(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/adapters/AllocationClassAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation.adapters; 18 | 19 | import org.objectweb.asm.ClassVisitor; 20 | import org.objectweb.asm.MethodVisitor; 21 | import org.objectweb.asm.Opcodes; 22 | import org.objectweb.asm.commons.LocalVariablesSorter; 23 | import org.objectweb.asm.commons.JSRInlinerAdapter; 24 | 25 | /** 26 | * Instruments bytecodes that allocateString heap memory to call a recording hook. 27 | * A ClassVisitor that processes methods with a 28 | * AllocationMethodAdapter to instrument heap allocations. 29 | * 30 | * @author jeremymanson@google.com (Jeremy Manson) 31 | * @author fischman@google.com (Ami Fischman) (Original Author) 32 | */ 33 | public class AllocationClassAdapter extends ClassVisitor { 34 | private final String recorderClass; 35 | private final String recorderMethod; 36 | 37 | public AllocationClassAdapter(final ClassVisitor cv, 38 | final String recorderClass, 39 | final String recorderMethod) { 40 | super(Opcodes.ASM5, cv); 41 | this.recorderClass = recorderClass; 42 | this.recorderMethod = recorderMethod; 43 | } 44 | 45 | /** 46 | * For each method in the class being instrumented, visitMethod 47 | * is called and the returned MethodVisitor is used to visit the method. 48 | * Note that a new MethodVisitor is constructed for each method. 49 | */ 50 | @Override 51 | public MethodVisitor visitMethod(final int access, 52 | final String base, 53 | final String desc, 54 | final String signature, 55 | final String[] exceptions) { 56 | MethodVisitor mv = cv.visitMethod(access, base, desc, signature, exceptions); 57 | if (mv != null) { 58 | // We need to compute stackmaps (see 59 | // AllocationInstrumenter#instrument). This can't really be 60 | // done for old bytecode that contains JSR and RET instructions. 61 | // So, we remove JSRs and RETs. 62 | final JSRInlinerAdapter jsria = new JSRInlinerAdapter(mv, access, base, desc, signature, exceptions); 63 | final AllocationMethodAdapter aimv = new AllocationMethodAdapter(jsria, recorderClass, recorderMethod); 64 | final LocalVariablesSorter lvs = new LocalVariablesSorter(access, desc, aimv); 65 | aimv.lvs = lvs; 66 | mv = lvs; 67 | } 68 | return mv; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/recorders/LifetimePrinter.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation.recorders; 2 | 3 | import com.google.monitoring.runtime.instrumentation.InstrumentationProperties; 4 | import com.google.monitoring.runtime.instrumentation.events.EventParser; 5 | import com.google.monitoring.runtime.instrumentation.events.LifetimeEvent; 6 | 7 | import java.io.BufferedWriter; 8 | import java.io.FileNotFoundException; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.OutputStreamWriter; 12 | import java.io.UnsupportedEncodingException; 13 | import java.io.Writer; 14 | import java.lang.management.GarbageCollectorMXBean; 15 | import java.lang.management.ManagementFactory; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.concurrent.BlockingQueue; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | /** 22 | * Created by jmaloney on 10/10/16. 23 | */ 24 | public class LifetimePrinter extends Thread { 25 | 26 | private BlockingQueue queue; 27 | private final Writer writer; 28 | private final List eventList = new ArrayList<>(); 29 | private long gcCount = 0; 30 | private GarbageCollectorMXBean gcBean; 31 | private final EventParser.VerbosityLevel verbosityLevel; 32 | 33 | public LifetimePrinter(final InstrumentationProperties properties) throws FileNotFoundException, UnsupportedEncodingException { 34 | writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(properties.outputPath()), "utf-8")); 35 | verbosityLevel = properties.verbosityLevel(); 36 | 37 | for(GarbageCollectorMXBean gc: ManagementFactory.getGarbageCollectorMXBeans()) { 38 | if (gc.getName().equals("ParNew") || gc.getName().equals("G1_Young_Generation")){ 39 | gcBean = gc; 40 | } 41 | } 42 | } 43 | 44 | @Override 45 | public void run() { 46 | Thread.currentThread().setName("LifetimePrinter"); 47 | for(;;){ 48 | try { 49 | if (gcCount < gcBean.getCollectionCount()){ 50 | processEventList(); 51 | queue.clear(); 52 | gcCount = gcBean.getCollectionCount(); 53 | } else { 54 | final LifetimeEvent event = queue.poll(1, TimeUnit.MILLISECONDS); 55 | if (event != null) { 56 | eventList.add(event); 57 | } 58 | } 59 | } catch (InterruptedException e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | } 64 | 65 | private void processEventList(){ 66 | try { 67 | writer.write("#GC "); 68 | writer.write(Long.toString(gcCount)); 69 | writer.write("\n"); 70 | for(int i = 0; i < eventList.size(); i++){ 71 | final LifetimeEvent event = eventList.get(i); 72 | if (event.alive()){ 73 | final String parsed = EventParser.parseEvent(event, verbosityLevel); 74 | writer.write(parsed); 75 | } 76 | } 77 | } catch (IOException e) { 78 | System.err.println("Exception in the LifetimePrinter thread printing!"); 79 | } 80 | eventList.clear(); 81 | } 82 | 83 | public void setQueue(final BlockingQueue queue) { 84 | this.queue = queue; 85 | } 86 | 87 | public void close() throws IOException { 88 | writer.flush(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/flame/FlameCollapse.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.flame; 2 | 3 | import java.io.*; 4 | import java.util.HashMap; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | 8 | /** 9 | * Utility used to process stacktrace data generated from the flamesampler into a format compatible with the 10 | * FlameGraph tools. This work largely consists of combining all like stacktraces and summing their values. 11 | * 12 | * Created by jmaloney on 5/27/2016. 13 | */ 14 | public class FlameCollapse { 15 | private final Map traces = new HashMap<>(); 16 | private final String outputFile = "collapsed.txt"; 17 | private String inputPath = "stacks.txt"; 18 | private long inputLineCount = 0; 19 | private String methodFilter = null; 20 | 21 | public static void main(String[] args) throws Exception { 22 | System.out.println("Begin FlameCollapse..."); 23 | FlameCollapse collapser = new FlameCollapse(); 24 | collapser.handleArgs(args); 25 | collapser.collapseFile(); 26 | collapser.writeData(); 27 | } 28 | 29 | public void handleArgs(final String[] args){ 30 | if (args.length == 1){ 31 | inputPath = args[0]; 32 | } else if (args.length == 2){ 33 | inputPath = args[0]; 34 | methodFilter = args[1].toLowerCase(); 35 | } 36 | } 37 | 38 | public void collapseFile(){ 39 | try { 40 | final BufferedReader br = new BufferedReader(new FileReader(inputPath)); 41 | String line; 42 | while ((line = br.readLine()) != null) { 43 | inputLineCount++; 44 | if (line.startsWith("#")){ 45 | continue; // skip "comment/meta" lines 46 | } 47 | 48 | processLine(traces, line); 49 | } 50 | br.close(); 51 | } catch (FileNotFoundException e) { 52 | System.err.println("File " + inputPath + " could not be found!"); 53 | System.exit(1); 54 | } catch (IOException e){ 55 | System.err.println("IOException while processing " + inputPath); 56 | e.printStackTrace(); 57 | System.exit(1); 58 | } 59 | } 60 | 61 | public void writeData(){ 62 | try { 63 | final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( 64 | new FileOutputStream(outputFile), "utf-8")); 65 | 66 | long outputCount = 0; 67 | final Iterator> it = traces.entrySet().iterator(); 68 | while (it.hasNext()) { 69 | Map.Entry entry = it.next(); 70 | outputCount++; 71 | writer.write(entry.getKey() + " " + entry.getValue() + "\n"); 72 | } 73 | writer.close(); 74 | System.out.println("Finished FlameCollapse! Collapsed " + inputLineCount + " rows into " + outputCount); 75 | } catch (IOException e){ 76 | System.err.println("IOException while writing output data!"); 77 | e.printStackTrace(); 78 | System.exit(1); 79 | } 80 | } 81 | 82 | private void processLine(final Map traces, final String line) { 83 | final String[] split = line.split(" "); 84 | if (split.length < 2){ 85 | System.out.println("Incomplete line(" + inputLineCount +"): " + line); 86 | return; 87 | } 88 | String key = split[0]; 89 | Long bytesAllocated = Long.valueOf(split[1]); 90 | if (methodFilter != null) { 91 | key = StackTraceParsers.squashTruncateNoFilter(key, methodFilter); 92 | } else { 93 | key = StackTraceParsers.squash(key); 94 | } 95 | 96 | //Ignore empty lines 97 | if (key.trim().equals("")) { 98 | return; 99 | } 100 | 101 | if (traces.containsKey(key)) { 102 | bytesAllocated = traces.get(key) + bytesAllocated; 103 | traces.put(key, bytesAllocated); 104 | } else { 105 | traces.put(key, bytesAllocated); 106 | } 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/ObjectSizeMeasurement.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation; 2 | 3 | import com.google.common.collect.ForwardingMap; 4 | import com.google.common.collect.MapMaker; 5 | 6 | import java.lang.instrument.Instrumentation; 7 | import java.util.Iterator; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentMap; 10 | import java.util.concurrent.ThreadLocalRandom; 11 | 12 | /** 13 | * Created by jmaloney on 10/10/16. 14 | */ 15 | public class ObjectSizeMeasurement { 16 | 17 | // Stores the object sizes for the last ~100000 encountered classes 18 | private static final ForwardingMap, Long> classSizesMap = 19 | new ForwardingMap, Long>() { 20 | private final ConcurrentMap, Long> map = new MapMaker() 21 | .weakKeys() 22 | .makeMap(); 23 | 24 | @Override 25 | public Map, Long> delegate() { 26 | return map; 27 | } 28 | 29 | // The approximate maximum size of the map 30 | private static final int MAX_SIZE = 100_000; 31 | 32 | // The approximate current size of the map; since this is not an AtomicInteger 33 | // and since we do not synchronize the updates to this field, it will only be 34 | // an approximate size of the map; it's good enough for our purposes though, 35 | // and not synchronizing the updates saves us some time 36 | private int approximateSize = 0; 37 | 38 | @Override 39 | public Long put(final Class key, final Long value) { 40 | // if we have too many elements, delete about 10% of them 41 | // this is expensive, but needs to be done to keep the map bounded 42 | // we also need to randomize the elements we delete: if we remove the same 43 | // elements all the time, we might end up adding them back to the map 44 | // immediately after, and then remove them again, then add them back, etc. 45 | // which will cause this expensive code to be executed too often 46 | if (approximateSize >= MAX_SIZE) { 47 | for (Iterator> it = keySet().iterator(); it.hasNext(); ) { 48 | it.next(); 49 | if (ThreadLocalRandom.current().nextDouble() < 0.1) { 50 | it.remove(); 51 | } 52 | } 53 | 54 | // get the exact size; another expensive call, but we need to correct 55 | // approximateSize every once in a while, or the difference between 56 | // approximateSize and the actual size might become significant over time; 57 | // the other solution is synchronizing every time we update approximateSize, 58 | // which seems even more expensive 59 | approximateSize = size(); 60 | } 61 | 62 | approximateSize++; 63 | return super.put(key, value); 64 | } 65 | }; 66 | 67 | 68 | /** 69 | * Returns the size of the given object. If the object is not an array, we 70 | * check the cache first, and update it as necessary. 71 | * 72 | * @param obj the object. 73 | * @param isArray indicates if the given object is an array. 74 | * @param instr the instrumentation object to use for finding the object size 75 | * @return the size of the given object. 76 | */ 77 | public static long getObjectSize(final Object obj, final boolean isArray, final Instrumentation instr) { 78 | if (isArray) { 79 | return instr.getObjectSize(obj); 80 | } 81 | 82 | final Class clazz = obj.getClass(); 83 | Long classSize = classSizesMap.get(clazz); 84 | if (classSize == null) { 85 | classSize = instr.getObjectSize(obj); 86 | classSizesMap.put(clazz, classSize); 87 | } 88 | 89 | return classSize; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/adapters/VerifyingClassAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation.adapters; 18 | 19 | import org.objectweb.asm.ClassVisitor; 20 | import org.objectweb.asm.ClassWriter; 21 | import org.objectweb.asm.MethodVisitor; 22 | import org.objectweb.asm.Opcodes; 23 | import org.objectweb.asm.commons.CodeSizeEvaluator; 24 | 25 | import java.util.logging.Level; 26 | import java.util.logging.Logger; 27 | 28 | /** 29 | * This is a class writer that gets used in place of the existing 30 | * {@link org.objectweb.asm.ClassWriter}, 31 | * and verifies properties of the class getting written. 32 | *

33 | * Currently, it only checks to see if the methods are of the correct length 34 | * for Java methods (<64K). 35 | * 36 | * @author jeremymanson@google.com (Jeremy Manson) 37 | */ 38 | public class VerifyingClassAdapter extends ClassVisitor { 39 | private static final Logger logger = Logger.getLogger(VerifyingClassAdapter.class.getName()); 40 | 41 | /** 42 | * An enum which indicates whether the class in question is verified. 43 | */ 44 | public enum State { 45 | PASS, UNKNOWN, FAIL_TOO_LONG; 46 | } 47 | 48 | final ClassWriter cw; 49 | final byte[] original; 50 | final String className; 51 | String message; 52 | State state; 53 | 54 | /** 55 | * @param cw A class writer that is wrapped by this class adapter 56 | * @param original the original bytecode 57 | * @param className the name of the class being examined. 58 | */ 59 | public VerifyingClassAdapter(final ClassWriter cw, 60 | final byte[] original, 61 | final String className) { 62 | super(Opcodes.ASM5, cw); 63 | state = State.UNKNOWN; 64 | message = "The class has not finished being examined"; 65 | this.cw = cw; 66 | this.original = original; 67 | this.className = className.replace('/', '.'); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | *

73 | * In addition, the returned {@link org.objectweb.asm.MethodVisitor} 74 | * will throw an exception if the method is greater than 64K in length. 75 | */ 76 | @Override 77 | public MethodVisitor visitMethod( 78 | final int access, 79 | final String name, 80 | final String desc, 81 | final String signature, 82 | final String[] exceptions) { 83 | final MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 84 | return new CodeSizeEvaluator(mv) { 85 | @Override 86 | public void visitEnd() { 87 | super.visitEnd(); 88 | if (getMaxSize() > 64 * 1024) { 89 | state = State.FAIL_TOO_LONG; 90 | message = "the method " + name + " was too long."; 91 | } 92 | } 93 | }; 94 | } 95 | 96 | /** 97 | * {@inheritDoc} 98 | */ 99 | @Override 100 | public void visitEnd() { 101 | super.visitEnd(); 102 | if (state == State.UNKNOWN) { 103 | state = State.PASS; 104 | } 105 | } 106 | 107 | /** 108 | * Gets the verification state of this class. 109 | * 110 | * @return true iff the class passed inspection. 111 | */ 112 | public boolean isVerified() { 113 | return state == State.PASS; 114 | } 115 | 116 | /** 117 | * Returns the byte array that contains the byte code for this class. 118 | * 119 | * @return a byte array. 120 | */ 121 | public byte[] toByteArray() { 122 | if (state != State.PASS) { 123 | logger.log(Level.WARNING, 124 | "Failed to instrument class " + className + " because " + message); 125 | return original; 126 | } 127 | return cw.toByteArray(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/InstrumentationPropertiesImpl.java: -------------------------------------------------------------------------------- 1 | package com.google.monitoring.runtime.instrumentation; 2 | 3 | import com.google.monitoring.runtime.instrumentation.events.EventParser; 4 | 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Created by jmaloney on 12/1/16. 12 | */ 13 | public class InstrumentationPropertiesImpl implements InstrumentationProperties { 14 | 15 | private final String recorder; 16 | private final boolean recordSize; 17 | private final String outputPath; 18 | private final EventParser.VerbosityLevel verbosityLevel; 19 | 20 | private final String sampleStrategy; 21 | private final long delaySecs; 22 | private final long sampleRate; 23 | private final long sampleInterval; 24 | 25 | public InstrumentationPropertiesImpl(final String filePath){ 26 | final Properties properties = loadProperties(filePath); 27 | 28 | recorder = loadString(properties, RECORDER_PROPERTY, DEFAULT_RECORDER); 29 | recordSize = loadBoolean(properties, RECORD_SIZE_PROPERTY, DEFAULT_RECORD_SIZE); 30 | outputPath = loadString(properties, OUTPUT_PATH_PROPERTY, DEFAULT_OUTPUT_PATH); 31 | verbosityLevel = loadVerbosity(properties, VERBOSITY_LEVEL_PROPERTY, DEFAULT_VERBOSITY_LEVEL); 32 | 33 | sampleStrategy = loadString(properties, SAMPLE_STRATEGY_PROPERTY, DEFAULT_SAMPLE_STRATEGY); 34 | delaySecs = loadLong(properties, DELAY_SECS_PROPERTY, DEFAULT_DELAY_SECS); 35 | sampleRate = loadLong(properties, SAMPLE_RATE_PROPERTY, DEFAULT_SAMPLE_RATE); 36 | sampleInterval = loadLong(properties, SAMPLE_INTERVAL_PROPERTY, DEFAULT_SAMPLE_INTERVAL); 37 | } 38 | 39 | private EventParser.VerbosityLevel loadVerbosity(Properties properties, String propertyName, EventParser.VerbosityLevel defaultValue) { 40 | final String verbosity = properties.getProperty(propertyName,""); 41 | switch (verbosity){ 42 | case "methodName": 43 | return EventParser.VerbosityLevel.METHOD_NAME; 44 | case "methodClassName": 45 | return EventParser.VerbosityLevel.METHOD_CLASS_NAME; 46 | case "methodClassLineNumber": 47 | return EventParser.VerbosityLevel.METHOD_CLASS_LINE_NUMBER; 48 | default: 49 | return defaultValue; 50 | } 51 | } 52 | 53 | private static String loadString(final Properties properties, final String propertyName, final String defaultValue){ 54 | final String value = properties.getProperty(propertyName); 55 | if (value != null){ 56 | return value; 57 | } else { 58 | return defaultValue; 59 | } 60 | } 61 | 62 | private static long loadLong(final Properties properties, final String propertyName, final long defaultValue){ 63 | final String value = properties.getProperty(propertyName); 64 | if (value != null){ 65 | return Long.parseLong(value); 66 | } else { 67 | return defaultValue; 68 | } 69 | } 70 | 71 | private static boolean loadBoolean(final Properties properties, final String propertyName, final boolean defaultValue){ 72 | final String value = properties.getProperty(propertyName); 73 | if (value != null){ 74 | return Boolean.parseBoolean(value); 75 | } else { 76 | return defaultValue; 77 | } 78 | } 79 | 80 | private static Properties loadProperties(final String filePath){ 81 | final Properties properties = new Properties(); 82 | if (filePath == null || filePath.trim().equals("")){ 83 | System.out.println("No Properties file specified. Using defaults."); 84 | } else { 85 | try { 86 | final InputStream inputStreamFromFile = new FileInputStream(filePath); 87 | properties.load(inputStreamFromFile); 88 | inputStreamFromFile.close(); 89 | } catch (IOException e) { 90 | e.printStackTrace(); 91 | System.out.println("Exception loading properties file! No Properties file loaded. Using defaults."); 92 | } 93 | 94 | } 95 | return properties; 96 | } 97 | 98 | @Override 99 | public String recorder() { 100 | return recorder; 101 | } 102 | 103 | @Override 104 | public boolean recordSize() { 105 | return recordSize; 106 | } 107 | 108 | @Override 109 | public String outputPath() { 110 | return outputPath; 111 | } 112 | 113 | @Override 114 | public EventParser.VerbosityLevel verbosityLevel() { 115 | return verbosityLevel; 116 | } 117 | 118 | @Override 119 | public String sampleStrategy() { 120 | return sampleStrategy; 121 | } 122 | 123 | @Override 124 | public long delaySecs() { 125 | return delaySecs; 126 | } 127 | 128 | @Override 129 | public long sampleRate() { 130 | return sampleRate; 131 | } 132 | 133 | @Override 134 | public long sampleInterval() { 135 | return sampleInterval; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation; 18 | 19 | import java.lang.instrument.Instrumentation; 20 | import java.util.concurrent.ThreadLocalRandom; 21 | import java.util.concurrent.atomic.AtomicLong; 22 | 23 | import com.google.monitoring.runtime.instrumentation.recorders.Recorder; 24 | import com.google.monitoring.runtime.instrumentation.sample.SampleStrategy; 25 | 26 | /** 27 | * The logic for recording allocations, called from bytecode rewritten by 28 | * {@link AllocationInstrumenter}. 29 | * 30 | * @author jeremymanson@google.com (Jeremy Manson) 31 | * @author fischman@google.com (Ami Fischman) 32 | */ 33 | public class AllocationRecorder { 34 | 35 | private static SampleStrategy sampleStrategy; 36 | private static Recorder recorder; 37 | 38 | static { 39 | // Sun's JVMs in 1.5.0_06 and 1.6.0{,_01} have a bug where calling 40 | // Instrumentation.getObjectSize() during JVM shutdown triggers a 41 | // JVM-crashing assert in JPLISAgent.c, so we make sure to not call it after 42 | // shutdown. There can still be a race here, depending on the extent of the 43 | // JVM bug, but this seems to be good enough. 44 | // instrumentation is volatile to make sure the threads reading it (in 45 | // recordAllocation()) see the updated value; we could do more 46 | // synchronization but it's not clear that it'd be worth it, given the 47 | // ambiguity of the bug we're working around in the first place. 48 | Runtime.getRuntime().addShutdownHook(new Thread() { 49 | @Override 50 | public void run() { 51 | setInstrumentation(null); 52 | } 53 | }); 54 | } 55 | 56 | // See the comment above the addShutdownHook in the static block above 57 | // for why this is volatile. 58 | private static volatile Instrumentation instrumentation = null; 59 | 60 | public static Instrumentation getInstrumentation() { 61 | return instrumentation; 62 | } 63 | 64 | static void setInstrumentation(final Instrumentation inst) { 65 | instrumentation = inst; 66 | } 67 | 68 | // Protects mutations of additionalSamplers. Reads are okay because 69 | // the field is volatile, so anyone who reads additionalSamplers 70 | // will get a consistent view of it. 71 | private static final Object samplerLock = new Object(); 72 | 73 | // Used for re-entrancy checks 74 | private static final ThreadLocal recordingAllocation = new ThreadLocal(); 75 | 76 | /** 77 | * Sets the {@link Recorder} that will get run every time an allocation is 78 | * sampled from Java code. Use this with extreme judiciousness! 79 | * 80 | * @param recorder The recorder to add. 81 | */ 82 | static void setRecorder(final Recorder recorder) { 83 | synchronized (samplerLock) { 84 | AllocationRecorder.recorder = recorder; 85 | } 86 | } 87 | 88 | /** 89 | * This is a helper method that calls the 3 param version. This is inserted into new allocations by the 90 | * instrumentation. 91 | * 92 | * @param cls class being sampled 93 | * @param newObj instance being sampled 94 | */ 95 | public static void recordAllocation(final Class cls, final Object newObj) { 96 | String typename = cls.getName(); 97 | recordAllocation(-1, typename, newObj); 98 | } 99 | 100 | /** 101 | * Records the allocation. This method is invoked on every allocation 102 | * performed by the system. 103 | * 104 | * @param count the count of how many instances are being 105 | * allocated, if an array is being allocated. If an array is not being 106 | * allocated, then this value will be -1. 107 | * @param desc the descriptor of the class/primitive type 108 | * being allocated. 109 | * @param newObj the new Object whose allocation is being 110 | * recorded. 111 | */ 112 | public static void recordAllocation(final int count, final String desc, final Object newObj) { 113 | // To prevent infinite sampling loop this is disabled while the sampler code is running 114 | if (recordingAllocation.get() == Boolean.TRUE) { 115 | return; 116 | } else { 117 | recordingAllocation.set(Boolean.TRUE); 118 | } 119 | 120 | try { 121 | if (sampleStrategy.canSample()){ 122 | recorder.record(count, desc, newObj); 123 | } 124 | } finally { 125 | recordingAllocation.set(Boolean.FALSE); 126 | } 127 | 128 | } 129 | 130 | /** 131 | * Sets the sampling strategy to use for sampling memory allocations. 132 | * 133 | * @param sampleStrategy an instance of SampleStrategy 134 | */ 135 | public static void setSampleStrategy(SampleStrategy sampleStrategy) { 136 | AllocationRecorder.sampleStrategy = sampleStrategy; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The memory allocation sampler uses a java agent based largely on the [Allocation Instrumentor](https://github.com/google/allocation-instrumenter) library. It outputs memory allocation data which can then be collapsed into a form which can be processed into a flamegraph using the [FlameGraph](https://github.com/brendangregg/FlameGraph) tools. 2 | 3 | ## Basic usage 4 | 5 | When starting the JVM to be instrumented the location of the java agent and the properties file must be provided using the following VM argument. 6 | 7 | ``` 8 | $ java -javaagent:/tmp/java-allocation-instrumenter-3.0-SNAPSHOT.jar=/tmp/flame.properties 9 | ``` 10 | 11 | The sampler will generate an output file (by default stacks.txt) which needs to be processed by the FlameCollapse tool (provided in this project) to convert it to the format needed by the FlameGraph tools. 12 | 13 | ``` 14 | $ java -jar target/java-allocation-instrumenter-3.0-SNAPSHOT.jar stacks.txt 15 | ``` 16 | 17 | Then finally run the FlameGraph tool on the previous output (by default collapsed.txt) to generate the flamegraph itself. 18 | 19 | ``` 20 | $ FlameGraph/flamegraph.pl collapsed.txt > flamegraph.svg 21 | ``` 22 | 23 | ## Package/Class Level Grouping And Filtering 24 | 25 | Often there are multiple code paths into a particular package or class and due to the fact that flamegraphs start at the bottom of the stack trace this will cause a particular packages memory allocation to spread all across the flamegraph. You can search the flamegraph with regex's (ctrl-f) and it will highlight the search for you but often this isn't enough. 26 | 27 | ![flamegraph](examples/highlighted_protobuf.png) 28 | 29 | To remedy this problem the FlameCollapse tool can consolidate all samples that lead to a specific package or class and filter out the rest. To use this simply provide the second parameter (it is NOT case sensitive). 30 | 31 | ``` 32 | $ java -jar target/java-allocation-instrumenter-3.0-SNAPSHOT.jar stacks.txt protobuf 33 | ``` 34 | 35 | This filters out all stack frames prior to entering the specified package or class and all samples that do not pass through the filter. The filtered output can then be used to generate a flamegraph containing a unified summary of data from that package. 36 | 37 | ![flamegraph](examples/group_by_protobuf.png) 38 | 39 | ## Building 40 | 41 | To build from source clone this repository and run 42 | 43 | ``` 44 | $ mvn clean package 45 | ``` 46 | 47 | The agent jar will be built in the `target` directory 48 | 49 | ``` 50 | $ ls -l target 51 | total 248 52 | drwxr-xr-x 16 jmaloney admin 544 Aug 18 10:07 apidocs 53 | drwxr-xr-x 4 jmaloney admin 136 Aug 18 10:07 classes 54 | drwxr-xr-x 3 jmaloney admin 102 Aug 18 10:07 generated-sources 55 | drwxr-xr-x 4 jmaloney admin 136 Aug 18 10:07 jarjar 56 | -rw-r--r-- 1 jmaloney admin 74471 Aug 18 10:07 java-allocation-instrumenter-3.0-SNAPSHOT-javadoc.jar 57 | -rw-r--r-- 1 jmaloney admin 34017 Aug 18 10:07 java-allocation-instrumenter-3.0-SNAPSHOT-sources.jar 58 | -rw-r--r-- 1 jmaloney admin 9327 Aug 18 10:07 java-allocation-instrumenter-3.0-SNAPSHOT.jar 59 | drwxr-xr-x 4 jmaloney admin 136 Aug 18 10:07 javadoc-bundle-options 60 | drwxr-xr-x 3 jmaloney admin 102 Aug 18 10:07 maven-archiver 61 | drwxr-xr-x 3 jmaloney admin 102 Aug 18 10:07 maven-status 62 | drwxr-xr-x 3 jmaloney admin 102 Aug 18 10:07 original-classes 63 | ``` 64 | 65 | The `java-allocation-instrumenter-3.0-SNAPSHOT.jar` file is the jar to specify in the javaagent parameter and the jar that contains the FlameCollapse utility. 66 | 67 | ## Sampling Strategies 68 | 69 | There are two sampling strategies that can be employed. The way they operate is fundamentally different. 70 | 71 | ### Allocation Count Strategy 72 | 73 | The allocation count strategy keeps a thread local counter of the number of allocations since the last sample and when that count is reached another sample is taken and the thread local counter set back to 0. 74 | 75 | ### Time Based Strategy 76 | 77 | The time based strategy sets a global timestamp which represents the earliest time the next sample can be taken. When an allocation occurs after this timestamp the global timestamp is set to the current time plus the minSampleInterval. 78 | 79 | ### Jitter 80 | 81 | To avoid sampling biases based on the periodic nature of programs every time a sample is taken the next time a sample can be taken has some randomness applied to it. In the case of the allocation count strategy this means that the number of allocations before the next sample is the 82 | 83 | `sample.rate + a random value between -sample.rate/2 and sample.rate/2` 84 | 85 | When using the time based strategy the next time a sample can be taken is 86 | 87 | `System.currentTimeMillis() + min.sample.interval + a random value between -min.sample.interval/2 and min.sample.interval/2` 88 | 89 | The average number of samples per second should still be the value set in the properties, however, there is some variance. 90 | 91 | ## Properties 92 | 93 | The sampler itself has the following properties that can be passed into the agent. 94 | 95 | Property Name | Values | Description 96 | --- | --- | --- 97 | recorder | flame/lifetime | Use either the flame of lifetime recorder 98 | output.file | filepath | Where to output the sampling data. 99 | stack.trace.verbosity | methodName/methodClassName/ methodClassLineNumber | How much detail about each stack frame in the sample is printed. 100 | record.size | true/false | Whether or not to estimate size of objects. 101 | sample.delay.secs | integer | Some applications have a long startup which you do not want to profile. This allows you delay all sampling for a specified amount of time. 102 | sample.strategy | time/allocationCount | The time based sampling strategy allows you to specify the minimum interval in between samples. The allocationCount strategy allows you to specify the number of allocations before taking the next sample (a count is kept per thread). 103 | sample.interval.ms | integer | The minimum time between samples. Only used when sample.strategy=time. 104 | sample.rate | integer | The number of allocations to wait before sampling. Only used when sample.strategy=allocationCount. 105 | 106 | ## Caveats 107 | 108 | This likely breaks all escape analysis done by the JVM and thus can/will report object allocation that would otherwise be optimized away. I have a few ideas on how to generate a profile of allocations that can be eliminated through escape analysis and then use this profile to prevent instrumentation of those allocations by the sampler but that is a work in progress at best. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 4.0.0 22 | 23 | org.sonatype.oss 24 | oss-parent 25 | 7 26 | 27 | 28 | com.google.code.java-allocation-instrumenter 29 | java-allocation-instrumenter 30 | jar 31 | 3.0-SNAPSHOT 32 | 33 | java-allocation-instrumenter 34 | 35 | A Java agent that rewrites bytecode to instrument allocation sites. 36 | 37 | 38 | 2009 39 | https://github.com/google/allocation-instrumenter/ 40 | 41 | 42 | Google, Inc. 43 | http://www.google.com 44 | 45 | 46 | 47 | 48 | Jeremy Manson 49 | Google Inc. 50 | 51 | 52 | 53 | 54 | 55 | The Apache Software License, Version 2.0 56 | http://www.apache.org/licenses/LICENSE-2.0.txt 57 | repo 58 | 59 | 60 | 61 | 62 | scm:git:https://github.com/google/allocation-instrumenter.git 63 | scm:git:git@github.com:google/allocation-instrumenter.git 64 | 65 | https://github.com/google/allocation-instrumenter/ 66 | 67 | 68 | 69 | Google Code Issue Tracking 70 | http://code.google.com/p/java-allocation-instrumenter/issues/list 71 | 72 | 73 | 74 | UTF-8 75 | 5.0.3 76 | true 77 | 78 | 79 | 80 | 81 | org.ow2.asm 82 | asm 83 | ${projectAsmVersion} 84 | 85 | 86 | org.ow2.asm 87 | asm-analysis 88 | ${projectAsmVersion} 89 | 90 | 91 | org.ow2.asm 92 | asm-commons 93 | ${projectAsmVersion} 94 | 95 | 96 | org.ow2.asm 97 | asm-tree 98 | ${projectAsmVersion} 99 | 100 | 101 | org.ow2.asm 102 | asm-util 103 | ${projectAsmVersion} 104 | 105 | 106 | org.ow2.asm 107 | asm-xml 108 | ${projectAsmVersion} 109 | 110 | 111 | com.google.guava 112 | guava 113 | 18.0 114 | 115 | 116 | junit 117 | junit 118 | RELEASE 119 | test 120 | 121 | 122 | 123 | 124 | package 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-compiler-plugin 130 | 3.2 131 | 132 | 1.7 133 | 1.7 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-eclipse-plugin 140 | 2.9 141 | 142 | true 143 | true 144 | ../eclipse-ws/ 145 | 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-release-plugin 151 | 2.5.1 152 | 153 | -DenableCiProfile=true 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-source-plugin 160 | 2.4 161 | 162 | 163 | attach-sources 164 | 165 | jar 166 | 167 | 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-javadoc-plugin 174 | 2.10.1 175 | 176 | 177 | attach-javadocs 178 | 179 | jar 180 | 181 | 182 | 183 | 184 | 185 | http://docs.oracle.com/javase/8/docs/api/ 186 | 187 | true 188 | public 189 | 190 | 191 | 192 | 193 | org.sonatype.plugins 194 | jarjar-maven-plugin 195 | 1.9 196 | 197 | 198 | embed-jars 199 | prepare-package 200 | 201 | jarjar 202 | 203 | 204 | 205 | org.ow2.asm:asm 206 | org.ow2.asm:asm-analysis 207 | org.ow2.asm:asm-commons 208 | org.ow2.asm:asm-tree 209 | org.ow2.asm:asm-util 210 | org.ow2.asm:asm-xml 211 | com.google.guava:guava 212 | 213 | 214 | 215 | org.objectweb.asm.** 216 | com.google.monitoring.runtime.instrumentation.asm.@1 217 | 218 | 219 | com.google.common.** 220 | com.google.monitoring.runtime.instrumentation.common.@0 221 | 222 | 223 | com.google.monitoring.runtime.instrumentation.common.collect.ComputingCache 224 | com.google.monitoring.runtime.instrumentation.common.collect.ComputingConcurrentHashMap 225 | com.google.monitoring.runtime.instrumentation.common.collect.ForwardingMap 226 | com.google.monitoring.runtime.instrumentation.common.collect.MapMaker 227 | com.google.monitoring.runtime.instrumentation.common.base.FunctionalEquivalence 228 | com.google.monitoring.runtime.instrumentation.common.base.PairwiseEquivalence 229 | com.google.monitoring.runtime.instrumentation.common.base.Supplier 230 | com.google.monitoring.runtime.instrumentation.common.base.Suppliers 231 | com.google.monitoring.runtime.instrumentation.common.primitives.Ints 232 | com.google.monitoring.runtime.instrumentation.asm.** 233 | com.google.monitoring.** 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | org.apache.maven.plugins 243 | maven-jar-plugin 244 | 2.5 245 | 246 | 247 | 248 | ./${project.artifactId}-${project.version}.${project.packaging} 249 | com.google.monitoring.runtime.instrumentation.AllocationInstrumenter 250 | true 251 | true 252 | com.google.monitoring.flame.FlameCollapse 253 | 254 | 255 | 256 | 257 | 258 | org.apache.maven.plugins 259 | maven-gpg-plugin 260 | 1.4 261 | 262 | 263 | sign-artifacts 264 | verify 265 | 266 | sign 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/StaticClassWriter.java: -------------------------------------------------------------------------------- 1 | /*** 2 | * ASM tests 3 | * Copyright (c) 2002-2005 France Telecom 4 | * All rights reserved. 5 | *

6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 3. Neither the name of the copyright holders nor the names of its 15 | * contributors may be used to endorse or promote products derived from 16 | * this software without specific prior written permission. 17 | *

18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 28 | * THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | // Portions Copyright 2011 Google, Inc. 32 | // 33 | // This is an extracted version of the ClassInfo and ClassWriter 34 | // portions of ClassWriterComputeFramesTest in the set of ASM tests. 35 | // We have done a fair bit of rewriting for readability, and changed 36 | // the comments. The original author is Eric Bruneton. 37 | 38 | 39 | package com.google.monitoring.runtime.instrumentation; 40 | 41 | import org.objectweb.asm.ClassReader; 42 | import org.objectweb.asm.ClassWriter; 43 | import org.objectweb.asm.Opcodes; 44 | import org.objectweb.asm.Type; 45 | 46 | import java.io.InputStream; 47 | import java.io.IOException; 48 | 49 | /** 50 | * A {@link ClassWriter} that looks for static class data in the 51 | * classpath when the classes are not available at runtime. 52 | * 53 | *

ClassWriter uses class hierarchy information, which it gets by 54 | * looking at loaded classes, to make some decisions about the best 55 | * way to write classes. The problem with this is that it fails if 56 | * the superclass hasn't been loaded yet. StaticClassWriter fails 57 | * over to looking for the class hierarchy information in the 58 | * ClassLoader's resources (usually the classpath) if the class it 59 | * needs hasn't been loaded yet. 60 | * 61 | *

This class was heavily influenced by ASM's 62 | * org.objectweb.asm.util.ClassWriterComputeFramesTest, which contains 63 | * the same logic in a subclass. The code here has been slightly 64 | * cleaned up for readability. 65 | * 66 | * @author jeremymanson@google.com (Jeremy Manson) 67 | */ 68 | class StaticClassWriter extends ClassWriter { 69 | 70 | /* The classloader that we use to look for the unloaded class */ 71 | private final ClassLoader classLoader; 72 | private final ClassReader classReader; 73 | 74 | /** 75 | * {@inheritDoc} 76 | * @param classLoader the class loader that loaded this class 77 | */ 78 | public StaticClassWriter(final ClassReader classReader, 79 | final int flags, 80 | final ClassLoader classLoader) { 81 | super(classReader, flags); 82 | this.classReader = classReader; 83 | this.classLoader = classLoader; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | @Override 90 | protected String getCommonSuperClass(final String type1, final String type2) { 91 | try { 92 | return super.getCommonSuperClass(type1, type2); 93 | } catch (Throwable e) { 94 | // Try something else... 95 | } 96 | // Exactly the same as in ClassWriter, but gets the superclass 97 | // directly from the class file. 98 | ClassInfo ci1, ci2; 99 | try { 100 | ci1 = new ClassInfo(type1, classLoader, classReader); 101 | ci2 = new ClassInfo(type2, classLoader, classReader); 102 | } catch (Throwable e) { 103 | throw new RuntimeException(e); 104 | } 105 | if (ci1.isAssignableFrom(ci2)) { 106 | return type1; 107 | } 108 | if (ci2.isAssignableFrom(ci1)) { 109 | return type2; 110 | } 111 | if (ci1.isInterface() || ci2.isInterface()) { 112 | return "java/lang/Object"; 113 | } 114 | 115 | do { 116 | // Should never be null, because if ci1 were the Object class 117 | // or an interface, it would have been caught above. 118 | ci1 = ci1.getSuperclass(); 119 | } while (!ci1.isAssignableFrom(ci2)); 120 | return ci1.getType().getInternalName(); 121 | } 122 | 123 | /** 124 | * For a given class, this stores the information needed by the 125 | * getCommonSuperClass test. This determines if the class is 126 | * available at runtime, and then, if it isn't, it tries to get the 127 | * class file, and extract the appropriate information from that. 128 | */ 129 | static class ClassInfo { 130 | 131 | private final Type type; 132 | private final ClassLoader loader; 133 | private final ClassReader classReader; 134 | private final boolean isInterface; 135 | private final String superClass; 136 | private final String[] interfaces; 137 | 138 | public ClassInfo(final String type, final ClassLoader loader, final ClassReader classReader) { 139 | this.classReader = classReader; 140 | Class cls = null; 141 | // First, see if we can extract the information from the class... 142 | try { 143 | cls = Class.forName(type); 144 | } catch (Exception e) { 145 | // failover... 146 | } 147 | 148 | if (cls != null) { 149 | this.type = Type.getType(cls); 150 | this.loader = loader; 151 | this.isInterface = cls.isInterface(); 152 | this.superClass = cls.getSuperclass().getName(); 153 | final Class[] ifs = cls.getInterfaces(); 154 | this.interfaces = new String[ifs.length]; 155 | for (int i = 0; i < ifs.length; i++) { 156 | this.interfaces[i] = ifs[i].getName(); 157 | } 158 | return; 159 | } 160 | 161 | // The class isn't loaded. Try to get the class file, and 162 | // extract the information from that. 163 | this.loader = loader; 164 | this.type = Type.getObjectType(type); 165 | final String fileName = type.replace('.', '/') + ".class"; 166 | InputStream is = null; 167 | ClassReader cr; 168 | try { 169 | is = (loader == null) ? 170 | ClassLoader.getSystemResourceAsStream(fileName) : 171 | loader.getResourceAsStream(fileName); 172 | cr = new ClassReader(is); 173 | } catch (IOException e) { 174 | cr = classReader; 175 | } finally { 176 | if (is != null) { 177 | try { 178 | is.close(); 179 | } catch (Exception e) { 180 | } 181 | } 182 | } 183 | 184 | int offset = cr.header; 185 | isInterface = (cr.readUnsignedShort(offset) & Opcodes.ACC_INTERFACE) != 0; 186 | final char[] buf = new char[2048]; 187 | 188 | // Read the superclass 189 | offset += 4; 190 | superClass = readConstantPoolString(cr, offset, buf); 191 | 192 | // Read the interfaces 193 | offset += 2; 194 | final int numInterfaces = cr.readUnsignedShort(offset); 195 | interfaces = new String[numInterfaces]; 196 | offset += 2; 197 | for (int i = 0; i < numInterfaces; i++) { 198 | interfaces[i] = readConstantPoolString(cr, offset, buf); 199 | offset += 2; 200 | } 201 | } 202 | 203 | String readConstantPoolString(final ClassReader cr, final int offset, final char[] buf) { 204 | final int cpIndex = cr.getItem(cr.readUnsignedShort(offset)); 205 | if (cpIndex == 0) { 206 | return null; 207 | // throw new RuntimeException("Bad constant pool index"); 208 | } 209 | return cr.readUTF8(cpIndex, buf); 210 | } 211 | 212 | Type getType() { 213 | return type; 214 | } 215 | 216 | ClassInfo getSuperclass() { 217 | if (superClass == null) { 218 | return null; 219 | } 220 | return new ClassInfo(superClass, loader, classReader); 221 | } 222 | 223 | /** 224 | * Same as {@link Class#getInterfaces()} 225 | */ 226 | ClassInfo[] getInterfaces() { 227 | if (interfaces == null) { 228 | return new ClassInfo[0]; 229 | } 230 | final ClassInfo[] result = new ClassInfo[interfaces.length]; 231 | for (int i = 0; i < result.length; ++i) { 232 | result[i] = new ClassInfo(interfaces[i], loader, classReader); 233 | } 234 | return result; 235 | } 236 | 237 | /** 238 | * Same as {@link Class#isInterface} 239 | */ 240 | boolean isInterface() { 241 | return isInterface; 242 | } 243 | 244 | private boolean implementsInterface(final ClassInfo that) { 245 | for (ClassInfo c = this; c != null; c = c.getSuperclass()) { 246 | for (ClassInfo iface : c.getInterfaces()) { 247 | if (iface.type.equals(that.type) || 248 | iface.implementsInterface(that)) { 249 | return true; 250 | } 251 | } 252 | } 253 | return false; 254 | } 255 | 256 | private boolean isSubclassOf(final ClassInfo that) { 257 | for (ClassInfo ci = this; ci != null; ci = ci.getSuperclass()) { 258 | if (ci.getSuperclass() != null && 259 | ci.getSuperclass().type.equals(that.type)) { 260 | return true; 261 | } 262 | } 263 | return false; 264 | } 265 | 266 | /** 267 | * Same as {@link Class#isAssignableFrom(Class)} 268 | */ 269 | boolean isAssignableFrom(final ClassInfo that) { 270 | return (this == that || 271 | that.isSubclassOf(this) || 272 | that.implementsInterface(this) || 273 | (that.isInterface() 274 | && getType().getDescriptor().equals("Ljava/lang/Object;"))); 275 | } 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/adapters/EscapeMethodAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation.adapters; 18 | 19 | import com.google.monitoring.runtime.instrumentation.AllocationInstrumenter; 20 | import org.objectweb.asm.Label; 21 | import org.objectweb.asm.MethodVisitor; 22 | import org.objectweb.asm.Opcodes; 23 | import org.objectweb.asm.Type; 24 | import org.objectweb.asm.commons.LocalVariablesSorter; 25 | 26 | import java.util.LinkedList; 27 | import java.util.List; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | /** 32 | * A MethodVisitor that instruments all heap allocation bytecodes 33 | * to record the allocation being done for profiling. 34 | * Instruments bytecodes that allocateString heap memory to call a recording hook. 35 | * 36 | * @author Ami Fischman 37 | */ 38 | class EscapeMethodAdapter extends MethodVisitor { 39 | 40 | public static final String START_SIGNATURE = "()V"; 41 | public static final String STOP_SIGNATURE= "(Ljava/land/String;)V"; 42 | public static final String CLASS_PATH = "com/google/monitoring/runtime/instrumentation/EscapeAnalyzer"; 43 | private int allocIndex = 0; 44 | 45 | // A helper struct for describing the scope of temporary local variables we 46 | // create as part of the instrumentation. 47 | private static class VariableScope { 48 | public final int index; 49 | public final Label start; 50 | public final Label end; 51 | public final String desc; 52 | 53 | public VariableScope(int index, Label start, Label end, String desc) { 54 | this.index = index; 55 | this.start = start; 56 | this.end = end; 57 | this.desc = desc; 58 | } 59 | } 60 | 61 | // Dictionary of primitive type opcode to english name. 62 | private static final String[] primitiveTypeNames = new String[]{ 63 | "INVALID0", "INVALID1", "INVALID2", "INVALID3", 64 | "boolean", "char", "float", "double", 65 | "byte", "short", "int", "long" 66 | }; 67 | 68 | // To track the difference between 's called as the result of a NEW 69 | // and 's called because of superclass initialization, we track the 70 | // number of NEWs that still need to have their 's called. 71 | private int outstandingAllocs = 0; 72 | 73 | // We need to set the scope of any local variables we materialize; 74 | // accumulate the scopes here and set them all at the end of the visit to 75 | // ensure all labels have been resolved. Allocated on-demand. 76 | private List localScopes = null; 77 | 78 | private List getLocalScopes() { 79 | if (localScopes == null) { 80 | localScopes = new LinkedList(); 81 | } 82 | return localScopes; 83 | } 84 | 85 | 86 | /** 87 | * The LocalVariablesSorter used in this adapter. Lame that it's public but 88 | * the ASM architecture requires setting it from the outside after this 89 | * AllocationMethodAdapter is fully constructed and the LocalVariablesSorter 90 | * constructor requires a reference to this adapter. The only setter of 91 | * this should be AllocationClassAdapter.visitMethod(). 92 | */ 93 | public LocalVariablesSorter lvs = null; 94 | 95 | /** 96 | * A new AllocationMethodAdapter is created for each method that gets visited. 97 | */ 98 | public EscapeMethodAdapter(MethodVisitor mv, String recorderClass, 99 | String recorderMethod) { 100 | super(Opcodes.ASM5, mv); 101 | } 102 | 103 | /** 104 | * newarray shows up as an instruction taking an int operand (the primitive 105 | * element type of the array) so we hook it here. 106 | */ 107 | @Override 108 | public void visitIntInsn(int opcode, int operand) { 109 | if (opcode == Opcodes.NEWARRAY) { 110 | // instack: ... count 111 | // outstack: ... aref 112 | if (operand >= 4 && operand <= 11) { 113 | invokeStart(); 114 | super.visitInsn(Opcodes.DUP); // -> stack: ... count count 115 | super.visitIntInsn(opcode, operand); // -> stack: ... count aref 116 | invokeRecordAllocation(primitiveTypeNames[operand]); 117 | // -> stack: ... aref 118 | } else { 119 | AllocationInstrumenter.logger.severe("NEWARRAY called with an invalid operand " + 120 | operand + ". Not instrumenting this allocation!"); 121 | super.visitIntInsn(opcode, operand); 122 | } 123 | } else { 124 | super.visitIntInsn(opcode, operand); 125 | } 126 | } 127 | 128 | /** 129 | * Reflection-based allocation (@see java.lang.reflect.Array#newInstance) is 130 | * triggered with a static method call (INVOKESTATIC), so we hook it here. 131 | * Class initialization is triggered with a constructor call (INVOKESPECIAL) 132 | * so we hook that here too as a proxy for the new bytecode which leaves an 133 | * uninitialized object on the stack that we're not allowed to touch. 134 | * {@link Object#clone} is also a call to INVOKESPECIAL, 135 | * and is hooked here. {@link Class#newInstance} and 136 | * {@link java.lang.reflect.Constructor#newInstance} are both 137 | * INVOKEVIRTUAL calls, so they are hooked here, as well. 138 | */ 139 | @Override 140 | public void visitMethodInsn(int opcode, String owner, String name, 141 | String signature, boolean itf) { 142 | 143 | if (opcode == Opcodes.INVOKESPECIAL) { 144 | if ("".equals(name) && outstandingAllocs > 0) { 145 | // Tricky because superclass initializers mean there can be more calls 146 | // to than calls to NEW; hence outstandingAllocs. 147 | --outstandingAllocs; 148 | 149 | // Most of the time (i.e. in bytecode generated by javac) it is the case 150 | // that following an call the top of the stack has a reference ot 151 | // the newly-initialized object. But nothing in the JVM Spec requires 152 | // this, so we need to play games with the stack to make an explicit 153 | // extra copy (and then discard it). 154 | 155 | dupStackElementBeforeSignatureArgs(signature); 156 | super.visitMethodInsn(opcode, owner, name, signature, itf); 157 | super.visitLdcInsn(-1); 158 | super.visitInsn(Opcodes.SWAP); 159 | invokeRecordAllocation(owner); 160 | super.visitInsn(Opcodes.POP); 161 | return; 162 | } 163 | } 164 | 165 | super.visitMethodInsn(opcode, owner, name, signature, itf); 166 | } 167 | 168 | // Given a method signature interpret the top of the stack as the arguments 169 | // to the method, dup the top-most element preceding these arguments, and 170 | // leave the arguments alone. This is done by inspecting each parameter 171 | // type, popping off the stack elements using the type information, 172 | // duplicating the target element, and pushing the arguments back on the 173 | // stack. 174 | private void dupStackElementBeforeSignatureArgs(final String sig) { 175 | final Label beginScopeLabel = new Label(); 176 | final Label endScopeLabel = new Label(); 177 | super.visitLabel(beginScopeLabel); 178 | 179 | Type[] argTypes = Type.getArgumentTypes(sig); 180 | int[] args = new int[argTypes.length]; 181 | 182 | for (int i = argTypes.length - 1; i >= 0; --i) { 183 | args[i] = newLocal(argTypes[i], beginScopeLabel, endScopeLabel); 184 | super.visitVarInsn(argTypes[i].getOpcode(Opcodes.ISTORE), args[i]); 185 | } 186 | super.visitInsn(Opcodes.DUP); 187 | for (int i = 0; i < argTypes.length; ++i) { 188 | int op = argTypes[i].getOpcode(Opcodes.ILOAD); 189 | super.visitVarInsn(op, args[i]); 190 | if (op == Opcodes.ALOAD) { 191 | super.visitInsn(Opcodes.ACONST_NULL); 192 | super.visitVarInsn(Opcodes.ASTORE, args[i]); 193 | } 194 | } 195 | super.visitLabel(endScopeLabel); 196 | } 197 | 198 | /** 199 | * new and anewarray bytecodes take a String operand for the type of 200 | * the object or array element so we hook them here. Note that new doesn't 201 | * actually result in any instrumentation here; we just do a bit of 202 | * book-keeping and do the instrumentation following the constructor call 203 | * (because we're not allowed to touch the object until it is initialized). 204 | */ 205 | @Override 206 | public void visitTypeInsn(int opcode, String typeName) { 207 | if (opcode == Opcodes.NEW) { 208 | // We can't actually tag this object right after allocation because it 209 | // must be initialized with a ctor before we can touch it (Verifier 210 | // enforces this). Instead, we just note it and tag following 211 | // initialization. 212 | invokeStart(); 213 | super.visitTypeInsn(opcode, typeName); 214 | ++outstandingAllocs; 215 | } else if (opcode == Opcodes.ANEWARRAY) { 216 | invokeStart(); 217 | super.visitInsn(Opcodes.DUP); 218 | super.visitTypeInsn(opcode, typeName); 219 | invokeRecordAllocation(typeName); 220 | } else { 221 | super.visitTypeInsn(opcode, typeName); 222 | } 223 | } 224 | 225 | private void invokeStart() { 226 | super.visitMethodInsn(Opcodes.INVOKESTATIC,CLASS_PATH,"start",START_SIGNATURE,false); 227 | } 228 | 229 | /** 230 | * Called by the ASM framework once the class is done being visited to 231 | * compute stack & local variable count maximums. 232 | */ 233 | @Override 234 | public void visitMaxs(int maxStack, int maxLocals) { 235 | if (localScopes != null) { 236 | for (VariableScope scope : localScopes) { 237 | super.visitLocalVariable("xxxxx$" + scope.index, scope.desc, null, 238 | scope.start, scope.end, scope.index); 239 | } 240 | } 241 | super.visitMaxs(maxStack, maxLocals); 242 | } 243 | 244 | // Helper method to allocateString a new local variable and account for its scope. 245 | private int newLocal(Type type, String typeDesc, 246 | Label begin, Label end) { 247 | int newVar = lvs.newLocal(type); 248 | getLocalScopes().add(new VariableScope(newVar, begin, end, typeDesc)); 249 | return newVar; 250 | } 251 | 252 | private int newLocal(Type type, Label begin, Label end) { 253 | return newLocal(type, type.getDescriptor(), begin, end); 254 | } 255 | 256 | private static final Pattern namePattern = 257 | Pattern.compile("^\\[*L([^;]+);$"); 258 | 259 | // Helper method to actually invoke the recorder function for an allocation 260 | // event. 261 | // pre: stack: ... count newobj 262 | // post: stack: ... newobj 263 | private void invokeRecordAllocation(String typeName) { 264 | allocIndex++; 265 | super.visitLdcInsn("methodNamePath:"+ allocIndex); 266 | super.visitMethodInsn(Opcodes.INVOKESTATIC, 267 | CLASS_PATH, "stop", STOP_SIGNATURE, false); 268 | } 269 | 270 | /** 271 | * multianewarray gets its very own visit method in the ASM framework, so we 272 | * hook it here. This bytecode is different from most in that it consumes a 273 | * variable number of stack elements during execution. The number of stack 274 | * elements consumed is specified by the dimCount operand. 275 | */ 276 | @Override 277 | public void visitMultiANewArrayInsn(String typeName, int dimCount) { 278 | // stack: ... dim1 dim2 dim3 ... dimN 279 | invokeStart(); 280 | super.visitMultiANewArrayInsn(typeName, dimCount); 281 | invokeRecordAllocation(typeName); 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/AllocationInstrumenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation; 18 | 19 | import com.google.monitoring.runtime.instrumentation.adapters.AllocationClassAdapter; 20 | import com.google.monitoring.runtime.instrumentation.adapters.VerifyingClassAdapter; 21 | import com.google.monitoring.runtime.instrumentation.events.AllocationEvent; 22 | import com.google.monitoring.runtime.instrumentation.recorders.FlamePrinter; 23 | import com.google.monitoring.runtime.instrumentation.recorders.FlameRecorder; 24 | import com.google.monitoring.runtime.instrumentation.events.LifetimeEvent; 25 | import com.google.monitoring.runtime.instrumentation.recorders.LifetimePrinter; 26 | import com.google.monitoring.runtime.instrumentation.recorders.LifetimeRecorder; 27 | import com.google.monitoring.runtime.instrumentation.sample.AllocationCountSampler; 28 | import com.google.monitoring.runtime.instrumentation.sample.PeriodicSampler; 29 | import com.google.monitoring.runtime.instrumentation.sample.SampleStrategy; 30 | import org.objectweb.asm.ClassVisitor; 31 | import org.objectweb.asm.ClassReader; 32 | import org.objectweb.asm.ClassWriter; 33 | 34 | import java.io.*; 35 | import java.lang.instrument.ClassFileTransformer; 36 | import java.lang.instrument.Instrumentation; 37 | import java.lang.instrument.UnmodifiableClassException; 38 | import java.security.ProtectionDomain; 39 | import java.util.ArrayList; 40 | import java.util.concurrent.ArrayBlockingQueue; 41 | import java.util.concurrent.BlockingQueue; 42 | import java.util.concurrent.TimeUnit; 43 | import java.util.logging.Level; 44 | import java.util.logging.Logger; 45 | 46 | /** 47 | * Instruments bytecodes that allocate heap memory to call a recording hook. 48 | * This will add a static invocation to a recorder function to any bytecode that 49 | * looks like it will be allocating heap memory allowing users to implement heap 50 | * profiling schemes. 51 | * 52 | * @author Ami Fischman 53 | * @author Jeremy Manson 54 | */ 55 | public class AllocationInstrumenter implements ClassFileTransformer { 56 | public static final Logger logger = Logger.getLogger(AllocationInstrumenter.class.getName()); 57 | 58 | // We can rewrite classes loaded by the bootstrap class loader 59 | // iff the agent is loaded by the bootstrap class loader. It is 60 | // always *supposed* to be loaded by the bootstrap class loader, but 61 | // this relies on the Boot-Class-Path attribute in the JAR file always being 62 | // set to the name of the JAR file that contains this agent, which we cannot 63 | // guarantee programmatically. 64 | private static volatile boolean canRewriteBootstrap; 65 | 66 | private static boolean canRewriteClass(final String className, final ClassLoader loader) { 67 | // There are two conditions under which we don't rewrite: 68 | // 1. If className was loaded by the bootstrap class loader and 69 | // the agent wasn't (in which case the class being rewritten 70 | // won't be able to call agent methods). 71 | // 2. If it is java.lang.ThreadLocal, which can't be rewritten because the 72 | // JVM depends on its structure. 73 | if (((loader == null) && !canRewriteBootstrap) || 74 | className.startsWith("java/lang/ThreadLocal")) { 75 | return false; 76 | } 77 | // third_party/java/webwork/*/ognl.jar contains bad class files. Ugh. 78 | if (className.startsWith("ognl/")) { 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | // No instantiating me except in premain() or in {@link JarClassTransformer}. 86 | AllocationInstrumenter() { 87 | } 88 | 89 | public static void premain(final String agentArgs, final Instrumentation inst) { 90 | System.out.println("Loading allocation instrumentation..."); 91 | AllocationRecorder.setInstrumentation(inst); 92 | 93 | // Force eager class loading here; we need these classes in order to do 94 | // instrumentation, so if we don't do the eager class loading, we 95 | // get a ClassCircularityError when trying to load and instrument 96 | // this class. 97 | try { 98 | Class.forName("sun.security.provider.PolicyFile"); 99 | Class.forName("java.util.ResourceBundle"); 100 | Class.forName("java.util.Date"); 101 | } catch (Throwable t) { 102 | // NOP 103 | } 104 | 105 | if (!inst.isRetransformClassesSupported()) { 106 | System.err.println("Some JDK classes are already loaded and " + 107 | "will not be instrumented."); 108 | } 109 | 110 | // Don't try to rewrite classes loaded by the bootstrap class 111 | // loader if this class wasn't loaded by the bootstrap class 112 | // loader. 113 | if (AllocationRecorder.class.getClassLoader() != null) { 114 | canRewriteBootstrap = false; 115 | // The loggers aren't installed yet, so we use println. 116 | System.err.println("Class loading breakage: " + 117 | "Will not be able to instrument JDK classes"); 118 | return; 119 | } 120 | canRewriteBootstrap = true; 121 | 122 | if (!setupRecorder(agentArgs)){ 123 | return; 124 | } 125 | 126 | bootstrap(inst); 127 | System.out.println("Loaded allocation instrumentation!"); 128 | } 129 | 130 | /** 131 | * Load the properties file and set the specified settings. 132 | * 133 | * @param propertiesPath path to properties file 134 | * @return true if sampler setup successful 135 | */ 136 | private static boolean setupRecorder(final String propertiesPath){ 137 | final InstrumentationProperties properties = new InstrumentationPropertiesImpl(propertiesPath); 138 | 139 | // Set sampling properties 140 | final long start = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(properties.delaySecs()); 141 | final SampleStrategy sampleStrategy; 142 | switch (properties.sampleStrategy()){ 143 | case "allocationCount": 144 | sampleStrategy = new AllocationCountSampler(start, properties.sampleRate(), properties.sampleRate()); 145 | break; 146 | case "time": 147 | sampleStrategy = new PeriodicSampler(start, properties.sampleInterval(), properties.sampleInterval()); 148 | break; 149 | default: 150 | System.err.println("Unknown sample strategy! " + properties.sampleStrategy() + " Stopping instrumentations."); 151 | return false; 152 | } 153 | AllocationRecorder.setSampleStrategy(sampleStrategy); 154 | 155 | // Setup recorder 156 | try{ 157 | switch (properties.recorder()){ 158 | case "flame": 159 | setupFlameSampler(properties); 160 | break; 161 | case "lifetime": 162 | setupLifetimeRecorder(properties); 163 | break; 164 | } 165 | } catch (Exception e) { 166 | e.printStackTrace(); 167 | System.err.println("Exception setting up the " + properties.recorder() + " recorder. Stopping instrumentation."); 168 | return false; 169 | } 170 | 171 | return true; 172 | } 173 | 174 | private static void setupLifetimeRecorder(final InstrumentationProperties properties) throws FileNotFoundException, UnsupportedEncodingException { 175 | final BlockingQueue queue = new ArrayBlockingQueue<>(2048); 176 | final LifetimePrinter printer = new LifetimePrinter(properties); 177 | printer.setQueue(queue); 178 | printer.start(); 179 | 180 | final LifetimeRecorder lifetimeRecorder = new LifetimeRecorder(queue, printer.getId(), properties.recordSize()); 181 | AllocationRecorder.setRecorder(lifetimeRecorder); 182 | 183 | Runtime.getRuntime().addShutdownHook(new Thread() { 184 | @Override 185 | public void run() { 186 | try { 187 | printer.close(); 188 | } catch (IOException e) { 189 | e.printStackTrace(); 190 | } 191 | } 192 | }); 193 | 194 | } 195 | 196 | private static void setupFlameSampler(final InstrumentationProperties properties) throws FileNotFoundException, UnsupportedEncodingException { 197 | final BlockingQueue queue = new ArrayBlockingQueue<>(2048); 198 | final FlamePrinter printer = new FlamePrinter(properties); 199 | printer.setQueue(queue); 200 | printer.start(); 201 | 202 | final FlameRecorder flameRecorder = new FlameRecorder(queue,printer.getId(), properties.recordSize()); 203 | AllocationRecorder.setRecorder(flameRecorder); 204 | 205 | Runtime.getRuntime().addShutdownHook(new Thread() { 206 | @Override 207 | public void run() { 208 | try { 209 | printer.close(); 210 | } catch (IOException e) { 211 | e.printStackTrace(); 212 | } 213 | } 214 | }); 215 | } 216 | 217 | private static void bootstrap(final Instrumentation inst) { 218 | inst.addTransformer(new AllocationInstrumenter(), 219 | inst.isRetransformClassesSupported()); 220 | 221 | if (!canRewriteBootstrap) { 222 | return; 223 | } 224 | 225 | // Get the set of already loaded classes that can be rewritten. 226 | final Class[] classes = inst.getAllLoadedClasses(); 227 | final ArrayList> classList = new ArrayList>(); 228 | for (int i = 0; i < classes.length; i++) { 229 | if (inst.isModifiableClass(classes[i])) { 230 | classList.add(classes[i]); 231 | } 232 | } 233 | 234 | // Reload classes, if possible. 235 | final Class[] workaround = new Class[classList.size()]; 236 | try { 237 | inst.retransformClasses(classList.toArray(workaround)); 238 | } catch (UnmodifiableClassException e) { 239 | System.err.println("AllocationInstrumenter was unable to " + 240 | "retransform early loaded classes."); 241 | } 242 | 243 | 244 | } 245 | 246 | @Override 247 | public byte[] transform(final ClassLoader loader, 248 | final String className, 249 | final Class classBeingRedefined, 250 | final ProtectionDomain protectionDomain, 251 | final byte[] origBytes) { 252 | if (!canRewriteClass(className, loader)) { 253 | return null; 254 | } 255 | 256 | return instrument(origBytes, loader); 257 | } 258 | 259 | 260 | /** 261 | * Given the bytes representing a class, go through all the bytecode in it and 262 | * instrument any occurrences of new/newarray/anewarray/multianewarray with 263 | * pre- and post-allocation hooks. Even more fun, intercept calls to the 264 | * reflection API's Array.newInstance() and instrument those too. 265 | * 266 | * @param originalBytes the original byte[] code. 267 | * @param recorderClass the String internal name of the class 268 | * containing the recorder method to run. 269 | * @param recorderMethod the String name of the recorder method 270 | * to run. 271 | * @param loader the ClassLoader for this class. 272 | * @return the instrumented byte[] code. 273 | */ 274 | public static byte[] instrument(final byte[] originalBytes, 275 | final String recorderClass, 276 | final String recorderMethod, 277 | final ClassLoader loader) { 278 | final ClassReader cr = new ClassReader(originalBytes); 279 | try { 280 | 281 | //Don't instrument this package except for its test class 282 | if (cr.getClassName().contains("com\\google\\monitoring\\runtime\\instrumentation\\") ){ 283 | if (!cr.getClassName().contains("Test")){ 284 | return originalBytes; 285 | } 286 | } 287 | 288 | // The verifier in JDK7+ requires accurate stackmaps, so we use 289 | // COMPUTE_FRAMES. 290 | final ClassWriter cw = new StaticClassWriter(cr, ClassWriter.COMPUTE_FRAMES, loader); 291 | 292 | final VerifyingClassAdapter vcw = new VerifyingClassAdapter(cw, originalBytes, cr.getClassName()); 293 | final ClassVisitor adapter = new AllocationClassAdapter(vcw, recorderClass, recorderMethod); 294 | 295 | cr.accept(adapter, ClassReader.SKIP_FRAMES); 296 | 297 | return vcw.toByteArray(); 298 | } catch (RuntimeException e) { 299 | logger.log(Level.WARNING, "Failed to instrument class.", e); 300 | throw e; 301 | } catch (Error e) { 302 | logger.log(Level.WARNING, "Failed to instrument class.", e); 303 | throw e; 304 | } 305 | } 306 | 307 | 308 | /** 309 | * @param originalBytes The original version of the class. 310 | * @param loader The ClassLoader of this class. 311 | * @return the instrumented version of this class. 312 | * @see #instrument(byte[], String, String, ClassLoader) 313 | * documentation for the 4-arg version. This is a convenience 314 | * version that uses the recorder in this class. 315 | */ 316 | public static byte[] instrument(final byte[] originalBytes, final ClassLoader loader) { 317 | return instrument( 318 | originalBytes, 319 | "com/google/monitoring/runtime/instrumentation/AllocationRecorder", 320 | "recordAllocation", 321 | loader); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/main/java/com/google/monitoring/runtime/instrumentation/adapters/AllocationMethodAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 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 | */ 16 | 17 | package com.google.monitoring.runtime.instrumentation.adapters; 18 | 19 | import com.google.monitoring.runtime.instrumentation.AllocationInstrumenter; 20 | import org.objectweb.asm.Label; 21 | import org.objectweb.asm.MethodVisitor; 22 | import org.objectweb.asm.Opcodes; 23 | import org.objectweb.asm.Type; 24 | import org.objectweb.asm.commons.LocalVariablesSorter; 25 | 26 | import java.util.LinkedList; 27 | import java.util.List; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | /** 32 | * A MethodVisitor that instruments all heap allocation bytecodes 33 | * to record the allocation being done for profiling. 34 | * Instruments bytecodes that allocateString heap memory to call a recording hook. 35 | * 36 | * @author Ami Fischman 37 | */ 38 | public class AllocationMethodAdapter extends MethodVisitor { 39 | /** 40 | * The signature string the recorder method must have. The method must be 41 | * static, return void, and take as arguments: 42 | *

    43 | *
  1. an int count of how many instances are being allocated. -1 means a 44 | * simple new to distinguish from a 1-element array. 0 shows up as a value 45 | * here sometimes; one reason is toArray()-type methods that require an array 46 | * type argument (see ArrayList.toArray() for example).
  2. 47 | *
  3. a String descriptor of the class/primitive type being allocated.
  4. 48 | *
  5. an Object reference to the just-allocated Object.
  6. 49 | *
50 | */ 51 | public static final String RECORDER_SIGNATURE = 52 | "(ILjava/lang/String;Ljava/lang/Object;)V"; 53 | 54 | /** 55 | * Like RECORDER_SIGNATURE, but for a method that extracts all of 56 | * the information dynamically from a class. 57 | */ 58 | public static final String CLASS_RECORDER_SIG = 59 | "(Ljava/lang/Class;Ljava/lang/Object;)V"; 60 | 61 | // A helper struct for describing the scope of temporary local variables we 62 | // create as part of the instrumentation. 63 | private static class VariableScope { 64 | public final int index; 65 | public final Label start; 66 | public final Label end; 67 | public final String desc; 68 | 69 | public VariableScope(final int index, 70 | final Label start, 71 | final Label end, 72 | final String desc) { 73 | this.index = index; 74 | this.start = start; 75 | this.end = end; 76 | this.desc = desc; 77 | } 78 | } 79 | 80 | // Dictionary of primitive type opcode to english name. 81 | private static final String[] primitiveTypeNames = new String[]{ 82 | "INVALID0", "INVALID1", "INVALID2", "INVALID3", 83 | "boolean", "char", "float", "double", 84 | "byte", "short", "int", "long" 85 | }; 86 | 87 | // To track the difference between 's called as the result of a NEW 88 | // and 's called because of superclass initialization, we track the 89 | // number of NEWs that still need to have their 's called. 90 | private int outstandingAllocs = 0; 91 | 92 | // We need to set the scope of any local variables we materialize; 93 | // accumulate the scopes here and set them all at the end of the visit to 94 | // ensure all labels have been resolved. Allocated on-demand. 95 | private List localScopes = null; 96 | 97 | private List getLocalScopes() { 98 | if (localScopes == null) { 99 | localScopes = new LinkedList(); 100 | } 101 | return localScopes; 102 | } 103 | 104 | private final String recorderClass; 105 | private final String recorderMethod; 106 | 107 | /** 108 | * The LocalVariablesSorter used in this adapter. Lame that it's public but 109 | * the ASM architecture requires setting it from the outside after this 110 | * AllocationMethodAdapter is fully constructed and the LocalVariablesSorter 111 | * constructor requires a reference to this adapter. The only setter of 112 | * this should be AllocationClassAdapter.visitMethod(). 113 | */ 114 | public LocalVariablesSorter lvs = null; 115 | 116 | // A new AllocationMethodAdapter is created for each method that gets visited. 117 | public AllocationMethodAdapter(final MethodVisitor mv, 118 | final String recorderClass, 119 | final String recorderMethod) { 120 | super(Opcodes.ASM5, mv); 121 | this.recorderClass = recorderClass; 122 | this.recorderMethod = recorderMethod; 123 | } 124 | 125 | /** 126 | * newarray shows up as an instruction taking an int operand (the primitive 127 | * element type of the array) so we hook it here. 128 | */ 129 | @Override 130 | public void visitIntInsn(final int opcode, final int operand) { 131 | if (opcode == Opcodes.NEWARRAY) { 132 | // instack: ... count 133 | // outstack: ... aref 134 | if (operand >= 4 && operand <= 11) { 135 | super.visitInsn(Opcodes.DUP); // -> stack: ... count count 136 | super.visitIntInsn(opcode, operand); // -> stack: ... count aref 137 | invokeRecordAllocation(primitiveTypeNames[operand]); 138 | // -> stack: ... aref 139 | } else { 140 | AllocationInstrumenter.logger.severe("NEWARRAY called with an invalid operand " + 141 | operand + ". Not instrumenting this allocation!"); 142 | super.visitIntInsn(opcode, operand); 143 | } 144 | } else { 145 | super.visitIntInsn(opcode, operand); 146 | } 147 | } 148 | 149 | // Helper method to compute class name as a String and push it on the stack. 150 | // pre: stack: ... class 151 | // post: stack: ... class className 152 | private void pushClassNameOnStack() { 153 | super.visitInsn(Opcodes.DUP); 154 | // -> stack: ... class class 155 | super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", 156 | "getName", "()Ljava/lang/String;", false); 157 | // -> stack: ... class classNameDotted 158 | super.visitLdcInsn('.'); 159 | // -> stack: ... class classNameDotted '.' 160 | super.visitLdcInsn('/'); 161 | // -> stack: ... class classNameDotted '.' '/' 162 | super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", 163 | "replace", "(CC)Ljava/lang/String;", false); 164 | // -> stack: ... class className 165 | } 166 | 167 | // Helper method to compute the product of an integer array and push it on 168 | // the stack. 169 | // pre: stack: ... intArray 170 | // post: stack: ... intArray product 171 | private void pushProductOfIntArrayOnStack() { 172 | final Label beginScopeLabel = new Label(); 173 | final Label endScopeLabel = new Label(); 174 | 175 | final int dimsArrayIndex = newLocal("[I", beginScopeLabel, endScopeLabel); 176 | final int counterIndex = newLocal("I", beginScopeLabel, endScopeLabel); 177 | final int productIndex = newLocal("I", beginScopeLabel, endScopeLabel); 178 | final Label loopLabel = new Label(); 179 | final Label endLabel = new Label(); 180 | 181 | super.visitLabel(beginScopeLabel); 182 | 183 | // stack: ... intArray 184 | super.visitVarInsn(Opcodes.ASTORE, dimsArrayIndex); 185 | // -> stack: ... 186 | 187 | // counter = 0 188 | super.visitInsn(Opcodes.ICONST_0); 189 | super.visitVarInsn(Opcodes.ISTORE, counterIndex); 190 | // product = 1 191 | super.visitInsn(Opcodes.ICONST_1); 192 | super.visitVarInsn(Opcodes.ISTORE, productIndex); 193 | // loop: 194 | super.visitLabel(loopLabel); 195 | // if index >= arraylength goto end: 196 | super.visitVarInsn(Opcodes.ILOAD, counterIndex); 197 | super.visitVarInsn(Opcodes.ALOAD, dimsArrayIndex); 198 | super.visitInsn(Opcodes.ARRAYLENGTH); 199 | super.visitJumpInsn(Opcodes.IF_ICMPGE, endLabel); 200 | // product = product * max(array[counter],1) 201 | super.visitVarInsn(Opcodes.ALOAD, dimsArrayIndex); 202 | super.visitVarInsn(Opcodes.ILOAD, counterIndex); 203 | super.visitInsn(Opcodes.IALOAD); 204 | super.visitInsn(Opcodes.DUP); 205 | final Label nonZeroDimension = new Label(); 206 | super.visitJumpInsn(Opcodes.IFNE, nonZeroDimension); 207 | super.visitInsn(Opcodes.POP); 208 | super.visitInsn(Opcodes.ICONST_1); 209 | super.visitLabel(nonZeroDimension); 210 | super.visitVarInsn(Opcodes.ILOAD, productIndex); 211 | super.visitInsn(Opcodes.IMUL); // if overflow happens it happens. 212 | super.visitVarInsn(Opcodes.ISTORE, productIndex); 213 | // iinc counter 1 214 | super.visitIincInsn(counterIndex, 1); 215 | // goto loop 216 | super.visitJumpInsn(Opcodes.GOTO, loopLabel); 217 | // end: 218 | super.visitLabel(endLabel); 219 | // re-push dimensions array 220 | super.visitVarInsn(Opcodes.ALOAD, dimsArrayIndex); 221 | // push product 222 | super.visitVarInsn(Opcodes.ILOAD, productIndex); 223 | 224 | super.visitLabel(endScopeLabel); 225 | } 226 | 227 | /** 228 | * Reflection-based allocation (@see java.lang.reflect.Array#newInstance) is 229 | * triggered with a static method call (INVOKESTATIC), so we hook it here. 230 | * Class initialization is triggered with a constructor call (INVOKESPECIAL) 231 | * so we hook that here too as a proxy for the new bytecode which leaves an 232 | * uninitialized object on the stack that we're not allowed to touch. 233 | * {@link java.lang.Object#clone} is also a call to INVOKESPECIAL, 234 | * and is hooked here. {@link java.lang.Class#newInstance} and 235 | * {@link java.lang.reflect.Constructor#newInstance} are both 236 | * INVOKEVIRTUAL calls, so they are hooked here, as well. 237 | */ 238 | @Override 239 | public void visitMethodInsn(final int opcode, 240 | final String owner, 241 | final String name, 242 | final String signature, 243 | final boolean itf) { 244 | if (opcode == Opcodes.INVOKESTATIC && 245 | // Array does its own native allocation. Grr. 246 | owner.equals("java/lang/reflect/Array") && 247 | name.equals("newInstance")) { 248 | if (signature.equals("(Ljava/lang/Class;I)Ljava/lang/Object;")) { 249 | 250 | final Label beginScopeLabel = new Label(); 251 | final Label endScopeLabel = new Label(); 252 | super.visitLabel(beginScopeLabel); 253 | 254 | // stack: ... class count 255 | final int countIndex = newLocal("I", beginScopeLabel, endScopeLabel); 256 | super.visitVarInsn(Opcodes.ISTORE, countIndex); 257 | // -> stack: ... class 258 | pushClassNameOnStack(); 259 | // -> stack: ... class className 260 | final int typeNameIndex = 261 | newLocal("Ljava/lang/String;", beginScopeLabel, endScopeLabel); 262 | super.visitVarInsn(Opcodes.ASTORE, typeNameIndex); 263 | // -> stack: ... class 264 | super.visitVarInsn(Opcodes.ILOAD, countIndex); 265 | // -> stack: ... class count 266 | super.visitMethodInsn(opcode, owner, name, signature, itf); 267 | // -> stack: ... newobj 268 | super.visitInsn(Opcodes.DUP); 269 | // -> stack: ... newobj newobj 270 | super.visitVarInsn(Opcodes.ILOAD, countIndex); 271 | // -> stack: ... newobj newobj count 272 | super.visitInsn(Opcodes.SWAP); 273 | // -> stack: ... newobj count newobj 274 | super.visitVarInsn(Opcodes.ALOAD, typeNameIndex); 275 | super.visitLabel(endScopeLabel); 276 | // -> stack: ... newobj count newobj className 277 | super.visitInsn(Opcodes.SWAP); 278 | // -> stack: ... newobj count className newobj 279 | super.visitMethodInsn(Opcodes.INVOKESTATIC, recorderClass, 280 | recorderMethod, RECORDER_SIGNATURE, false); 281 | // -> stack: ... newobj 282 | return; 283 | } else if (signature.equals("(Ljava/lang/Class;[I)Ljava/lang/Object;")) { 284 | final Label beginScopeLabel = new Label(); 285 | final Label endScopeLabel = new Label(); 286 | super.visitLabel(beginScopeLabel); 287 | 288 | final int dimsArrayIndex = newLocal("[I", beginScopeLabel, endScopeLabel); 289 | // stack: ... class dimsArray 290 | pushProductOfIntArrayOnStack(); 291 | // -> stack: ... class dimsArray product 292 | final int productIndex = newLocal("I", beginScopeLabel, endScopeLabel); 293 | super.visitVarInsn(Opcodes.ISTORE, productIndex); 294 | // -> stack: ... class dimsArray 295 | 296 | super.visitVarInsn(Opcodes.ASTORE, dimsArrayIndex); 297 | // -> stack: ... class 298 | pushClassNameOnStack(); 299 | // -> stack: ... class className 300 | final int typeNameIndex = 301 | newLocal("Ljava/lang/String;", beginScopeLabel, endScopeLabel); 302 | super.visitVarInsn(Opcodes.ASTORE, typeNameIndex); 303 | // -> stack: ... class 304 | super.visitVarInsn(Opcodes.ALOAD, dimsArrayIndex); 305 | // -> stack: ... class dimsArray 306 | super.visitMethodInsn(opcode, owner, name, signature, itf); 307 | // -> stack: ... newobj 308 | 309 | super.visitInsn(Opcodes.DUP); 310 | // -> stack: ... newobj newobj 311 | super.visitVarInsn(Opcodes.ILOAD, productIndex); 312 | // -> stack: ... newobj newobj product 313 | super.visitInsn(Opcodes.SWAP); 314 | // -> stack: ... newobj product newobj 315 | super.visitVarInsn(Opcodes.ALOAD, typeNameIndex); 316 | super.visitLabel(endScopeLabel); 317 | // -> stack: ... newobj product newobj className 318 | super.visitInsn(Opcodes.SWAP); 319 | // -> stack: ... newobj product className newobj 320 | super.visitMethodInsn(Opcodes.INVOKESTATIC, recorderClass, 321 | recorderMethod, RECORDER_SIGNATURE, false); 322 | // -> stack: ... newobj 323 | return; 324 | } 325 | } 326 | 327 | if (opcode == Opcodes.INVOKEVIRTUAL) { 328 | if ("clone".equals(name) && owner.startsWith("[")) { 329 | super.visitMethodInsn(opcode, owner, name, signature, itf); 330 | 331 | int i = 0; 332 | while (i < owner.length()) { 333 | if (owner.charAt(i) != '[') { 334 | break; 335 | } 336 | i++; 337 | } 338 | if (i > 1) { 339 | // -> stack: ... newobj 340 | super.visitTypeInsn(Opcodes.CHECKCAST, owner); 341 | // -> stack: ... arrayref 342 | calculateArrayLengthAndDispatch(owner.substring(i), i); 343 | } else { 344 | // -> stack: ... newobj 345 | super.visitInsn(Opcodes.DUP); 346 | // -> stack: ... newobj newobj 347 | super.visitTypeInsn(Opcodes.CHECKCAST, owner); 348 | // -> stack: ... newobj arrayref 349 | super.visitInsn(Opcodes.ARRAYLENGTH); 350 | // -> stack: ... newobj length 351 | super.visitInsn(Opcodes.SWAP); 352 | // -> stack: ... length newobj 353 | invokeRecordAllocation(owner.substring(i)); 354 | } 355 | return; 356 | } else if ("newInstance".equals(name)) { 357 | if ("java/lang/Class".equals(owner) && 358 | "()Ljava/lang/Object;".equals(signature)) { 359 | super.visitInsn(Opcodes.DUP); 360 | // -> stack: ... Class Class 361 | super.visitMethodInsn(opcode, owner, name, signature, itf); 362 | // -> stack: ... Class newobj 363 | super.visitInsn(Opcodes.DUP_X1); 364 | // -> stack: ... newobj Class newobj 365 | super.visitMethodInsn(Opcodes.INVOKESTATIC, recorderClass, 366 | recorderMethod, CLASS_RECORDER_SIG, false); 367 | // -> stack: ... newobj 368 | return; 369 | } else if ("java/lang/reflect/Constructor".equals(owner) && 370 | "([Ljava/lang/Object;)Ljava/lang/Object;".equals(signature)) { 371 | buildRecorderFromObject(opcode, owner, name, signature, itf); 372 | return; 373 | } 374 | } 375 | } 376 | 377 | if (opcode == Opcodes.INVOKESPECIAL) { 378 | if ("clone".equals(name) && "java/lang/Object".equals(owner)) { 379 | buildRecorderFromObject(opcode, owner, name, signature, itf); 380 | return; 381 | } else if ("".equals(name) && outstandingAllocs > 0) { 382 | // Tricky because superclass initializers mean there can be more calls 383 | // to than calls to NEW; hence outstandingAllocs. 384 | --outstandingAllocs; 385 | 386 | // Most of the time (i.e. in bytecode generated by javac) it is the case 387 | // that following an call the top of the stack has a reference ot 388 | // the newly-initialized object. But nothing in the JVM Spec requires 389 | // this, so we need to play games with the stack to make an explicit 390 | // extra copy (and then discard it). 391 | 392 | dupStackElementBeforeSignatureArgs(signature); 393 | super.visitMethodInsn(opcode, owner, name, signature, itf); 394 | super.visitLdcInsn(-1); 395 | super.visitInsn(Opcodes.SWAP); 396 | invokeRecordAllocation(owner); 397 | super.visitInsn(Opcodes.POP); 398 | return; 399 | } 400 | } 401 | 402 | super.visitMethodInsn(opcode, owner, name, signature, itf); 403 | } 404 | 405 | // This is the instrumentation that occurs when there is no static 406 | // information about the class we are instantiating. First we build the 407 | // object, then we get the class and invoke the recorder. 408 | private void buildRecorderFromObject(final int opcode, 409 | final String owner, 410 | final String name, 411 | final String signature, 412 | final boolean itf) { 413 | super.visitMethodInsn(opcode, owner, name, signature, itf); 414 | // -> stack: ... newobj 415 | super.visitInsn(Opcodes.DUP); 416 | // -> stack: ... newobj newobj 417 | super.visitInsn(Opcodes.DUP); 418 | // -> stack: ... newobj newobj newobj 419 | // We could be instantiating this class or a subclass, so we 420 | // have to get the class the hard way. 421 | super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", 422 | "()Ljava/lang/Class;", false); 423 | // -> stack: ... newobj newobj Class 424 | super.visitInsn(Opcodes.SWAP); 425 | // -> stack: ... newobj Class newobj 426 | super.visitMethodInsn(Opcodes.INVOKESTATIC, recorderClass, recorderMethod, 427 | CLASS_RECORDER_SIG, false); 428 | // -> stack: ... newobj 429 | } 430 | 431 | // Given a method signature interpret the top of the stack as the arguments 432 | // to the method, dup the top-most element preceding these arguments, and 433 | // leave the arguments alone. This is done by inspecting each parameter 434 | // type, popping off the stack elements using the type information, 435 | // duplicating the target element, and pushing the arguments back on the 436 | // stack. 437 | private void dupStackElementBeforeSignatureArgs(final String sig) { 438 | final Label beginScopeLabel = new Label(); 439 | final Label endScopeLabel = new Label(); 440 | super.visitLabel(beginScopeLabel); 441 | 442 | final Type[] argTypes = Type.getArgumentTypes(sig); 443 | final int[] args = new int[argTypes.length]; 444 | 445 | for (int i = argTypes.length - 1; i >= 0; --i) { 446 | args[i] = newLocal(argTypes[i], beginScopeLabel, endScopeLabel); 447 | super.visitVarInsn(argTypes[i].getOpcode(Opcodes.ISTORE), args[i]); 448 | } 449 | super.visitInsn(Opcodes.DUP); 450 | for (int i = 0; i < argTypes.length; ++i) { 451 | final int op = argTypes[i].getOpcode(Opcodes.ILOAD); 452 | super.visitVarInsn(op, args[i]); 453 | if (op == Opcodes.ALOAD) { 454 | super.visitInsn(Opcodes.ACONST_NULL); 455 | super.visitVarInsn(Opcodes.ASTORE, args[i]); 456 | } 457 | } 458 | super.visitLabel(endScopeLabel); 459 | } 460 | 461 | /** 462 | * new and anewarray bytecodes take a String operand for the type of 463 | * the object or array element so we hook them here. Note that new doesn't 464 | * actually result in any instrumentation here; we just do a bit of 465 | * book-keeping and do the instrumentation following the constructor call 466 | * (because we're not allowed to touch the object until it is initialized). 467 | */ 468 | @Override 469 | public void visitTypeInsn(final int opcode, final String typeName) { 470 | if (opcode == Opcodes.NEW) { 471 | // We can't actually tag this object right after allocation because it 472 | // must be initialized with a ctor before we can touch it (Verifier 473 | // enforces this). Instead, we just note it and tag following 474 | // initialization. 475 | super.visitTypeInsn(opcode, typeName); 476 | ++outstandingAllocs; 477 | } else if (opcode == Opcodes.ANEWARRAY) { 478 | super.visitInsn(Opcodes.DUP); 479 | super.visitTypeInsn(opcode, typeName); 480 | invokeRecordAllocation(typeName); 481 | } else { 482 | super.visitTypeInsn(opcode, typeName); 483 | } 484 | } 485 | 486 | /** 487 | * Called by the ASM framework once the class is done being visited to 488 | * compute stack and local variable count maximums. 489 | */ 490 | @Override 491 | public void visitMaxs(final int maxStack, final int maxLocals) { 492 | if (localScopes != null) { 493 | for (VariableScope scope : localScopes) { 494 | super.visitLocalVariable("xxxxx$" + scope.index, scope.desc, null, 495 | scope.start, scope.end, scope.index); 496 | } 497 | } 498 | super.visitMaxs(maxStack, maxLocals); 499 | } 500 | 501 | // Helper method to allocateString a new local variable and account for its scope. 502 | private int newLocal(final Type type, 503 | final String typeDesc, 504 | final Label begin, 505 | final Label end) { 506 | final int newVar = lvs.newLocal(type); 507 | getLocalScopes().add(new VariableScope(newVar, begin, end, typeDesc)); 508 | return newVar; 509 | } 510 | 511 | // Sometimes I happen to have a string descriptor and sometimes a type; 512 | // these alternate versions let me avoid recomputing whatever I already 513 | // know. 514 | private int newLocal(final String typeDescriptor, final Label begin, final Label end) { 515 | return newLocal(Type.getType(typeDescriptor), typeDescriptor, begin, end); 516 | } 517 | 518 | private int newLocal(final Type type, final Label begin, final Label end) { 519 | return newLocal(type, type.getDescriptor(), begin, end); 520 | } 521 | 522 | private static final Pattern namePattern = 523 | Pattern.compile("^\\[*L([^;]+);$"); 524 | 525 | // Helper method to actually invoke the recorder function for an allocation 526 | // event. 527 | // pre: stack: ... count newobj 528 | // post: stack: ... newobj 529 | private void invokeRecordAllocation(String typeName) { 530 | final Matcher matcher = namePattern.matcher(typeName); 531 | if (matcher.find()) { 532 | typeName = matcher.group(1); 533 | } 534 | // stack: ... count newobj 535 | super.visitInsn(Opcodes.DUP_X1); 536 | // -> stack: ... newobj count newobj 537 | super.visitLdcInsn(typeName); 538 | // -> stack: ... newobj count newobj typename 539 | super.visitInsn(Opcodes.SWAP); 540 | // -> stack: ... newobj count typename newobj 541 | super.visitMethodInsn(Opcodes.INVOKESTATIC, 542 | recorderClass, recorderMethod, RECORDER_SIGNATURE, false); 543 | // -> stack: ... newobj 544 | } 545 | 546 | /** 547 | * multianewarray gets its very own visit method in the ASM framework, so we 548 | * hook it here. This bytecode is different from most in that it consumes a 549 | * variable number of stack elements during execution. The number of stack 550 | * elements consumed is specified by the dimCount operand. 551 | */ 552 | @Override 553 | public void visitMultiANewArrayInsn(final String typeName, final int dimCount) { 554 | // stack: ... dim1 dim2 dim3 ... dimN 555 | super.visitMultiANewArrayInsn(typeName, dimCount); 556 | // -> stack: ... aref 557 | calculateArrayLengthAndDispatch(typeName, dimCount); 558 | } 559 | 560 | void calculateArrayLengthAndDispatch(final String typeName, final int dimCount) { 561 | // Since the dimensions of the array are not known at instrumentation 562 | // time, we take the created multi-dimensional array and peel off nesting 563 | // levels from the left. For each nesting layer we probe the array length 564 | // and accumulate a partial product which we can then feed the recording 565 | // function. 566 | 567 | // below we note the partial product of dimensions 1 to X-1 as productToX 568 | // (so productTo1 == 1 == no dimensions yet). We denote by aref0 the 569 | // array reference at the current nesting level (the containing aref's [0] 570 | // element). If we hit a level whose arraylength is 0 there's no point 571 | // continuing so we shortcut out. 572 | final Label zeroDimension = new Label(); 573 | super.visitInsn(Opcodes.DUP); // -> stack: ... origaref aref0 574 | super.visitLdcInsn(1); // -> stack: ... origaref aref0 productTo1 575 | for (int i = 0; i < dimCount; ++i) { 576 | // pre: stack: ... origaref aref0 productToI 577 | super.visitInsn(Opcodes.SWAP); // -> stack: ... origaref productToI aref 578 | super.visitInsn(Opcodes.DUP_X1); 579 | // -> stack: ... origaref aref0 productToI aref 580 | super.visitInsn(Opcodes.ARRAYLENGTH); 581 | // -> stack: ... origaref aref0 productToI dimI 582 | 583 | final Label nonZeroDimension = new Label(); 584 | super.visitInsn(Opcodes.DUP); 585 | // -> stack: ... origaref aref0 productToI dimI dimI 586 | super.visitJumpInsn(Opcodes.IFNE, nonZeroDimension); 587 | // -> stack: ... origaref aref0 productToI dimI 588 | super.visitInsn(Opcodes.POP); 589 | // -> stack: ... origaref aref0 productToI 590 | super.visitJumpInsn(Opcodes.GOTO, zeroDimension); 591 | super.visitLabel(nonZeroDimension); 592 | // -> stack: ... origaref aref0 productToI max(dimI,1) 593 | 594 | super.visitInsn(Opcodes.IMUL); 595 | // -> stack: ... origaref aref0 productTo{I+1} 596 | if (i < dimCount - 1) { 597 | super.visitInsn(Opcodes.SWAP); 598 | // -> stack: ... origaref productTo{I+1} aref0 599 | super.visitInsn(Opcodes.ICONST_0); 600 | // -> stack: ... origaref productTo{I+1} aref0 0 601 | super.visitInsn(Opcodes.AALOAD); 602 | // -> stack: ... origaref productTo{I+1} aref0' 603 | super.visitInsn(Opcodes.SWAP); 604 | } 605 | // post: stack: ... origaref aref0 productTo{I+1} 606 | } 607 | super.visitLabel(zeroDimension); 608 | 609 | super.visitInsn(Opcodes.SWAP); // -> stack: ... origaref product aref0 610 | super.visitInsn(Opcodes.POP); // -> stack: ... origaref product 611 | super.visitInsn(Opcodes.SWAP); // -> stack: ... product origaref 612 | invokeRecordAllocation(typeName); 613 | } 614 | } 615 | --------------------------------------------------------------------------------