package com.jetbrains.actionscript.profiler.model; import com.intellij.javascript.flex.mxml.schema.CodeContext; import com.intellij.lang.javascript.psi.JSCommonTypeNames; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.jetbrains.actionscript.profiler.sampler.*; import org.jetbrains.annotations.Nullable; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; public class ProfilingConnection { private static final Logger LOG = Logger.getInstance(ProfilingConnection.class.getName()); private ServerSocket myServerSocket; private ServerSocket myPolicyServerSocket; private OutputStream myOutputStream; private DataInputStream myInputStream; private PacketProcessor myCurrentPacketProcessor; private final Map<String, PacketProcessor> myInitialString2ProcessorsMap = new HashMap<>(); private final Callback myIoHandler; private final int myPort; private static final int ourAgentVersion = 4; private boolean myAbortingSocketConnection; private boolean myDisposed; public ProfilingConnection(int port, ProfilerDataConsumer sampleProcessor, Callback ioHandler) { myPort = port; myInitialString2ProcessorsMap.put( PolicyFileRequestProcessor.POLICY_FILE_REQUEST, new PolicyFileRequestProcessor(port) ); BatchSamplesProcessor samplesProcessor = new BatchSamplesProcessor(sampleProcessor); myInitialString2ProcessorsMap.put( BatchSamplesProcessor.BATCH_MARKER, samplesProcessor ); myInitialString2ProcessorsMap.put( BatchSamplesProcessor.CREATE_OBJECT_SAMPLE_MARKER, samplesProcessor ); myInitialString2ProcessorsMap.put( BatchSamplesProcessor.SAMPLE_MARKER, samplesProcessor ); myInitialString2ProcessorsMap.put( BatchSamplesProcessor.DELETE_OBJECT_SAMPLE_MARKER, samplesProcessor ); myInitialString2ProcessorsMap.put( FinishCommandProcessor.END_COMMAND_MARKER, new FinishCommandProcessor() ); myInitialString2ProcessorsMap.put( VersionHandShakeProcessor.VERSION_COMMAND_MARKER, new VersionHandShakeProcessor() ); myInitialString2ProcessorsMap.put( SampleInfoProcessor.COMMAND_MARKER, new SampleInfoProcessor(sampleProcessor) ); myIoHandler = ioHandler; } void connect() { myAbortingSocketConnection = false; myDisposed = false; ensurePolicyServedEvenOnFlashSecurityPort(); try { myServerSocket = new ServerSocket(myPort); Socket socket = myServerSocket.accept(); myInputStream = new DataInputStream(new BufferedInputStream(socket.getInputStream())); myOutputStream = socket.getOutputStream(); myServerSocket.close(); myServerSocket = null; myIoHandler.finished("Connection established", null); } catch (IOException ex) { final boolean abortedWaitingForConnection = ex instanceof SocketException && myAbortingSocketConnection; if (abortedWaitingForConnection) { ex = new EOFException("aborted wait for connection"); } else { LOG.warn(ex); } myIoHandler.finished(null, ex); return; } ApplicationManager.getApplication().executeOnPooledThread(() -> { int bytesRead = 0; try { while (true) { String x = myInputStream.readUTF(); if (x == null) break; LOG.debug(x); bytesRead += x.length(); try { if (myCurrentPacketProcessor == null) { String marker = x; int i = x.indexOf('\0'); if (i != -1) marker = x.substring(0, i + 1); myCurrentPacketProcessor = myInitialString2ProcessorsMap.get(marker); if (myCurrentPacketProcessor != null) { myCurrentPacketProcessor.startingPacket(x); } } if (myCurrentPacketProcessor != null) { PacketProcessor.ProcessingResult processingResult = myCurrentPacketProcessor.process(x); if (processingResult == PacketProcessor.ProcessingResult.FINISHED) myCurrentPacketProcessor = null; if (processingResult == PacketProcessor.ProcessingResult.STOP) return; } else { LOG.warn("No processing:" + x); } } catch (Exception e) { LOG.error(e); } } } catch (IOException ex) { LOG.debug("Bytes read:" + bytesRead); myIoHandler.finished(null, ex); } catch (Throwable t) { LOG.error(t); } }); } private void ensurePolicyServedEvenOnFlashSecurityPort() { ApplicationManager.getApplication().executeOnPooledThread(() -> { try { myPolicyServerSocket = new ServerSocket(843); while (true) { final Socket socket = myPolicyServerSocket.accept(); final OutputStream outputStream = socket.getOutputStream(); outputStream.write(policyFileRequestAnswer(myPort).getBytes()); LOG.debug("policy served from 843"); outputStream.close(); } } catch (IOException e) { if (e instanceof SocketException && myAbortingSocketConnection) return; if (myPolicyServerSocket != null) LOG.error(e); // myPolicyServerSocket == null is bind failed } }); } private static final int START_CPU_PROFILING = 1; private static final int STOP_CPU_PROFILING = 2; private static final int CAPTURE_MEMORY_SNAPSHOT = 3; private static final int DO_GC = 4; private static final int START_COLLECTING_LIVE_OBJECTS = 5; private static final int STOP_COLLECTING_LIVE_OBJECTS = 6; private void clearProfilingState() { final PacketProcessor processor = myInitialString2ProcessorsMap.get(BatchSamplesProcessor.BATCH_MARKER); ((BatchSamplesProcessor)processor).clearProfilingState(); } interface Callback { void finished(@Nullable String data, @Nullable IOException ex); } private final LinkedList<Callback> callbacks = new LinkedList<>(); public void stopCpuProfiling(final Callback callback) throws IOException { simpleCommand(new Callback() { public void finished(@Nullable String data, @Nullable IOException ex) { callback.finished(data, ex); clearProfilingState(); } }, STOP_CPU_PROFILING); } public void startCpuProfiling(Callback callback) throws IOException { simpleCommand(callback, START_CPU_PROFILING); } public void captureMemorySnapshot(Callback callback) throws IOException { simpleCommand(callback, CAPTURE_MEMORY_SNAPSHOT); } private void simpleCommand(Callback callback, int startCpuProfiling) throws IOException { if (myDisposed) { return; } synchronized (myOutputStream) { callbacks.addLast(callback); myOutputStream.write(startCpuProfiling); myOutputStream.flush(); } } public void doGc(Callback callback) throws IOException { simpleCommand(callback, DO_GC); } public void startCollectingLiveObjects(ProfilingManager.Callback callback) throws IOException { simpleCommand(callback, START_COLLECTING_LIVE_OBJECTS); } public void stopCollectingLiveObjects(ProfilingManager.Callback callback) throws IOException { simpleCommand(callback, STOP_COLLECTING_LIVE_OBJECTS); } public void dispose() throws IOException { if (myDisposed) return; myAbortingSocketConnection = true; myDisposed = true; if (myServerSocket != null) myServerSocket.close(); if (myOutputStream != null) myOutputStream.close(); if (myInputStream != null) myInputStream.close(); if (myPolicyServerSocket != null) myPolicyServerSocket.close(); } abstract static class PacketProcessor { enum ProcessingResult { CONTINUE, FINISHED, STOP } void startingPacket(String output) { } abstract ProcessingResult process(String output) throws IOException; } class PolicyFileRequestProcessor extends PacketProcessor { static final String POLICY_FILE_REQUEST = "<policy-file-request/>\0"; private final int myPort; PolicyFileRequestProcessor(int port) { myPort = port; } @Override ProcessingResult process(String output) throws IOException { String s = policyFileRequestAnswer(myPort); synchronized (myOutputStream) { LOG.debug("policy served"); // TODO merge with FlexUnit code myOutputStream.write(s.getBytes()); myOutputStream.flush(); connect(); return ProcessingResult.STOP; } } } private static String policyFileRequestAnswer(int port) { return "<?xml version=\"1.0\"?> \n" + "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd\">\n" + "<cross-domain-policy>\n" + " <allow-access-from domain=\"*\" to-ports=\"" + port + "\" />\n" + "</cross-domain-policy>\0"; } static class BatchSamplesProcessor extends PacketProcessor { private static final String BATCH_MARKER = "b\0"; private static final String SAMPLE_MARKER = "s\0"; private static final String CREATE_OBJECT_SAMPLE_MARKER = "c\0"; private static final String DELETE_OBJECT_SAMPLE_MARKER = "d\0"; private final ProfilerDataConsumer mySampleProcessor; private long sampleDuration = -1; private int frameIndex; private final Map<String, String> dictionary = new HashMap<>(1000); private final Map<String, String> typeDictionary = new HashMap<>(1000); private FrameInfo[] frames; private String type; private String specialArgs; static final int INDEX = SAMPLE_MARKER.length(); private int cpuSamples; private int memorySamples; private Sample lastCpuSample; private Sample lastCreateObjectSample; private final FrameInfoBuilder frameInfoBuilder = new FrameInfoBuilder(); public BatchSamplesProcessor(ProfilerDataConsumer sampleProcessor) { this.mySampleProcessor = sampleProcessor; } @Override ProcessingResult process(String output) throws IOException { if (frameIndex == -1) { if (output.startsWith(BATCH_MARKER)) return ProcessingResult.FINISHED; int i = INDEX + 1; //output.indexOf(' ', INDEX); final boolean cpuSample = output.startsWith(SAMPLE_MARKER); if (cpuSample) { i = output.indexOf(' ', INDEX); sampleDuration = Long.parseLong(output.substring(INDEX, i)); i += 2; } if (cpuSample || output.startsWith(CREATE_OBJECT_SAMPLE_MARKER) || output.startsWith(DELETE_OBJECT_SAMPLE_MARKER)) { int i2 = output.indexOf(' ', i); int frameCount = Integer.parseInt(output.substring(i - 1, i2 != -1 ? i2 : output.length())); frames = frameCount > 0 ? new FrameInfo[frameCount] : FrameInfo.EMPTY_FRAME_INFO_ARRAY; frameIndex = 0; type = output; specialArgs = i2 != -1 ? output.substring(i2 + 1) : ""; return maybeFinishSample(); } } if (frames != null && frameIndex >= 0 && frameIndex < frames.length) { char ch = output.charAt(0); if (output.startsWith("u>:")) { int count = Integer.parseInt(output.substring(output.indexOf(':') + 1)); Sample s = type.startsWith(CREATE_OBJECT_SAMPLE_MARKER) ? lastCreateObjectSample : type.startsWith(SAMPLE_MARKER) ? lastCpuSample : null; for (int i = s.frames.length - count; i < s.frames.length; ++i) { frames[frameIndex++] = s.frames[i]; } } else { if (Character.isDigit(ch)) { output = dictionary.get(output); } else { dictionary.put("" + (dictionary.size() + 1), output); } frames[frameIndex++] = frameInfoBuilder.buildInstance(output); } return maybeFinishSample(); } LOG.warn("Unexpected:" + output); return ProcessingResult.FINISHED; } private ProcessingResult maybeFinishSample() { if (frameIndex == frames.length) { Sample sample; if (type.startsWith(CREATE_OBJECT_SAMPLE_MARKER)) { ++memorySamples; final int endIndex = specialArgs.indexOf(' '); final int endIndex2 = specialArgs.indexOf(' ', endIndex + 1); final int id = Integer.parseInt(specialArgs.substring(0, endIndex)); String className = specialArgs.substring(endIndex + 1, endIndex2); className = getClassName(className); final int size = Integer.parseInt(specialArgs.substring(endIndex2 + 1)); sample = new CreateObjectSample( sampleDuration, frames, id, className, size ); lastCreateObjectSample = sample; } else if (type.startsWith(DELETE_OBJECT_SAMPLE_MARKER)) { ++memorySamples; final int endIndex = specialArgs.indexOf(' '); int endIndex2 = specialArgs.indexOf(' ', endIndex + 1); if (endIndex2 == -1) endIndex2 = specialArgs.length(); final int id = Integer.parseInt(specialArgs.substring(0, endIndex)); String type = specialArgs.substring(endIndex + 1, endIndex2); final int size = endIndex2 != specialArgs.length() ? Integer.parseInt(specialArgs.substring(endIndex2 + 1)) : 0; if (type != null) { type = getClassName(type); mySampleProcessor.process(new DeleteObjectSample(sampleDuration, frames, id, type, size)); } return ProcessingResult.FINISHED; } else { ++cpuSamples; sample = new Sample(sampleDuration, frames); lastCpuSample = sample; } mySampleProcessor.process(sample); frameIndex = -1; return ProcessingResult.FINISHED; } else { return ProcessingResult.CONTINUE; } } private String getClassName(String className) { if (Character.isDigit(className.charAt(0))) { className = typeDictionary.get(className); } else { className = className.replace("::", "."); if (className.startsWith(CodeContext.AS3_VEC_VECTOR_QUALIFIED_NAME)) { className = JSCommonTypeNames.VECTOR_CLASS_NAME + className.substring(CodeContext.AS3_VEC_VECTOR_QUALIFIED_NAME.length()); } typeDictionary.put(String.valueOf(typeDictionary.size()), className); } return className; } void startingPacket(String output) { if (output.startsWith(BATCH_MARKER)) { if (LOG.isDebugEnabled()) { LOG.debug(output + "," + System.currentTimeMillis() + "," + cpuSamples + "," + memorySamples); } memorySamples = 0; cpuSamples = 0; } frameIndex = -1; frames = null; type = null; specialArgs = null; } private void clearProfilingState() { dictionary.clear(); typeDictionary.clear(); lastCpuSample = null; lastCreateObjectSample = null; cpuSamples = 0; memorySamples = 0; } } class FinishCommandProcessor extends PacketProcessor { static final String END_COMMAND_MARKER = "e\0"; @Override ProcessingResult process(String output) throws IOException { Callback callback; synchronized (myOutputStream) { callback = callbacks.removeFirst(); } callback.finished(output, null); return ProcessingResult.FINISHED; } } class VersionHandShakeProcessor extends PacketProcessor { static final String VERSION_COMMAND_MARKER = "v\0"; @Override ProcessingResult process(String output) throws IOException { if (Integer.parseInt(output.substring(output.lastIndexOf(' ') + 1)) != ourAgentVersion) { LOG.warn("Version mismatch"); myIoHandler.finished(null, new AgentVersionMismatchProblem()); myOutputStream.close(); myInputStream.close(); } return ProcessingResult.FINISHED; } } private static class SampleInfoProcessor extends PacketProcessor { public static final String COMMAND_MARKER = "si\0"; private final ProfilerDataConsumer myDataConsumer; SampleInfoProcessor(ProfilerDataConsumer dataConsumer) { myDataConsumer = dataConsumer; } @Override ProcessingResult process(String output) throws IOException { if (output.startsWith("EndSnapshot")) return ProcessingResult.FINISHED; if (output.startsWith(COMMAND_MARKER)) return ProcessingResult.CONTINUE; if (output.startsWith("cls:")) { return ProcessingResult.CONTINUE; } int i = output.indexOf(','); int id = Integer.parseInt(output.substring(0, i)); while (i != -1) { int nextI = output.indexOf(',', i + 1); if (nextI == -1) nextI = output.length(); int nextId = Integer.parseInt(output.substring(i + 1, nextI)); myDataConsumer.referenced(id, nextId); if (nextI == output.length()) break; i = nextI; } return ProcessingResult.CONTINUE; } } }