com.google.speedtracer.client.model.JavaScriptProfileModelV8Impl.java Source code

Java tutorial

Introduction

Here is the source code for com.google.speedtracer.client.model.JavaScriptProfileModelV8Impl.java

Source

/*
 * Copyright 2010 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.speedtracer.client.model;

import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.coreext.client.IterableFastStringMap;
import com.google.gwt.coreext.client.JSOArray;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.speedtracer.client.ClientConfig;
import com.google.speedtracer.client.Logging;
import com.google.speedtracer.client.model.V8SymbolTable.AliasableEntry;
import com.google.speedtracer.client.model.V8SymbolTable.V8Symbol;
import com.google.speedtracer.client.util.Csv;
import com.google.speedtracer.client.util.WorkQueue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

/**
 * Parses the v8 profiler data from Chromium on behalf of
 * {@link JavaScriptProfile}.
 */
public class JavaScriptProfileModelV8Impl extends JavaScriptProfileModelImpl {

    /**
     * Used to store actions for parsing lines in the log.
     */
    interface LogAction {
        void doAction(JsArrayString logLine);
    }

    /**
     * Dummy entry in the action table that has no implementation.
     */
    class UnimplementedCommandMethod implements LogAction {
        private final String commandName;

        UnimplementedCommandMethod(String commandName) {
            this.commandName = commandName;
        }

        public void doAction(JsArrayString logLine) {
            if (ClientConfig.isDebugMode()) {
                Logging.getLogger().logText("Unimplemented command: " + commandName + " alias: " + logLine.get(0));
            }
        }
    }

    private static class ActionType extends AliasableEntry {
        public ActionType(String name, int value) {
            super(name, value);
        }
    }

    private static class DebugStats {
        public int addCollisions;
        public int lookupMisses;
        public int moveMisses;
        public int removeMisses;
    }

    /**
     * Wraps the {@link #procLogLines} method to run the processing of the log
     * iteratively using the work queue.
     */
    private class LogLineWorker implements WorkQueue.Node {
        public int currentOffset;
        public final JSOArray<String> logLines;
        public final UiEvent refRecord;

        public LogLineWorker(JSOArray<String> logLines, UiEvent refRecord, int currentOffset) {
            this.logLines = logLines;
            this.refRecord = refRecord;
            this.currentOffset = currentOffset;
        }

        public void execute() {
            processLogLines(refRecord, logLines, currentOffset);
        }

        public String getDescription() {
            int refSequence = (refRecord == null) ? -1 : refRecord.getSequence();
            return "LogLineWorkQueueNode seq " + refSequence + " offset " + currentOffset;
        }
    }

    /**
     * Introduces a new profile to be processed on the work queue.
     */
    private class NewProfileDataWorker implements WorkQueue.Node {
        public final JavaScriptProfile profile;
        public final JavaScriptProfileEvent rawEvent;
        public final UiEvent refRecord;

        public NewProfileDataWorker(UiEvent refRecord, JavaScriptProfile profile, JavaScriptProfileEvent rawEvent) {
            this.refRecord = refRecord;
            this.profile = profile;
            this.rawEvent = rawEvent;
        }

        public void execute() {
            currentProfile = profile;
            JSOArray<String> logLines = JSOArray.splitString(rawEvent.getProfileData(), "\n");
            workQueue.prepend(new LogLineWorker(logLines, refRecord, 0));
        }

        public String getDescription() {
            int refSequence = (refRecord == null) ? -1 : refRecord.getSequence();
            return "NewProfileDataWorkQueueNode seq " + refSequence;
        }
    }

    private static class V8SymbolType extends AliasableEntry {
        public V8SymbolType(String name, int value) {
            super(name, value);
        }
    }

    public static final String ADDRESS_TAG_CODE = "code";
    public static final String ADDRESS_TAG_CODE_MOVE = "code-move";
    public static final String ADDRESS_TAG_SCRATCH = "scratch";
    public static final String ADDRESS_TAG_STACK = "stack";

    /**
     * String constant in the data that identifies the profile data record as
     * coming from v8.
     */
    public static final String FORMAT = "v8";

    public static final int ACTION_TYPE_ALIAS = 1;
    public static final int ACTION_TYPE_PROFILER = 2;
    public static final int ACTION_TYPE_CODE_CREATION = 3;
    public static final int ACTION_TYPE_CODE_MOVE = 4;
    public static final int ACTION_TYPE_CODE_DELETE = 5;
    public static final int ACTION_TYPE_TICK = 6;
    public static final int ACTION_TYPE_REPEAT = 7;

    public static final int SYMBOL_TYPE_BUILTIN = 8;
    public static final int SYMBOL_TYPE_CALL_DEBUG_BREAK = 9;
    public static final int SYMBOL_TYPE_CALL_DEBUG_PREPARE_STEP_IN = 10;
    public static final int SYMBOL_TYPE_CALL_IC = 11;
    public static final int SYMBOL_TYPE_CALL_INITIALIZE = 12;
    public static final int SYMBOL_TYPE_CALL_MEGAMORPHIC = 13;
    public static final int SYMBOL_TYPE_CALL_MISS = 14;
    public static final int SYMBOL_TYPE_CALL_NORMAL = 15;
    public static final int SYMBOL_TYPE_PRE_MONOMORPHIC = 16;
    public static final int SYMBOL_TYPE_CALLBACK = 17;
    public static final int SYMBOL_TYPE_EVAL = 18;
    public static final int SYMBOL_TYPE_FUNCTION = 19;
    public static final int SYMBOL_TYPE_LOAD_IC = 20;
    public static final int SYMBOL_TYPE_KEYED_CALL_IC = 21;
    public static final int SYMBOL_TYPE_KEYED_LOAD_IC = 22;
    public static final int SYMBOL_TYPE_KEYED_STORE_IC = 23;
    public static final int SYMBOL_TYPE_LAZY_COMPILE = 24;
    public static final int SYMBOL_TYPE_REG_EXP = 25;
    public static final int SYMBOL_TYPE_SCRIPT = 26;
    public static final int SYMBOL_TYPE_STORE_IC = 27;
    public static final int SYMBOL_TYPE_STUB = 28;

    public static final int VM_STATE_JS = 0;
    public static final int VM_STATE_GC = 1;
    public static final int VM_STATE_OTHER = 3;
    public static final int VM_STATE_COMPILER = 2;
    public static final int VM_STATE_EXTERNAL = 4;

    static final DebugStats debugStats = new DebugStats();

    private static Element scrubbingDiv = Document.get().createDivElement();

    // TODO(zundel): this method is just for debugging. Not for production use.
    static void getProfileBreakdownText(StringBuilder result, JavaScriptProfileEvent rawEvent) {
        HashMap<String, Integer> commandMap = new HashMap<String, Integer>();
        String profileData = rawEvent.getProfileData();
        if (profileData == null || profileData.length() == 0) {
            return;
        }

        String[] logLines = profileData.split("\n");
        for (int i = 0, logLinesLength = logLines.length; i < logLinesLength; ++i) {
            String logLine = logLines[i];
            String[] logEntries = logLine.split(",");
            String command = logEntries[0];
            Integer count = commandMap.get(command);
            if (count == null) {
                commandMap.put(command, 1);
            } else {
                commandMap.put(command, count + 1);
            }
        }

        // Dump the command map as the output
        for (Entry<String, Integer> entry : commandMap.entrySet()) {
            result.append("Command: " + entry.getKey() + " " + entry.getValue() + "\n");
        }
    }

    private Map<String, ActionType> actionTypeMap = new HashMap<String, ActionType>();
    private Map<String, Double> addressTags = new HashMap<String, Double>();
    private JavaScriptProfile currentProfile = null;
    private final IterableFastStringMap<V8Symbol> lazyCompiledSymbols = new IterableFastStringMap<V8Symbol>();
    private Map<ActionType, LogAction> logActions = new HashMap<ActionType, LogAction>();
    private V8LogDecompressor logDecompressor = null;
    private V8SymbolTable symbolTable = new V8SymbolTable();
    private Map<String, V8SymbolType> symbolTypeMap = new HashMap<String, V8SymbolType>();
    private final WorkQueue workQueue;

    public JavaScriptProfileModelV8Impl(WorkQueue workQueue) {
        super("v8");
        this.workQueue = workQueue;
        populateAddressTags();
        populateActionTypes();
        populateSymbolTypes();
        // TODO (zundel): We could populate chrome's C++ symbols from nm or windows
        // .map files
    }

    @Override
    public String getDebugDumpHtml() {
        StringBuilder output = new StringBuilder();
        output.append("<h3>Debug Stats</h3>\n");
        output.append("<table>\n");
        output.append("<tr><td>Add Collisions</td><td>" + debugStats.addCollisions + "</td></tr>");
        output.append("<tr><td>Lookup Misses</td><td>" + debugStats.lookupMisses + "</td></tr>");
        output.append("<tr><td>Remove Misses</td><td>" + debugStats.removeMisses + "</td></tr>");
        output.append("<tr><td>Move Misses</td><td>" + debugStats.moveMisses + "</td></tr>");
        output.append("</table>\n");

        // TODO(zundel): Put this behind a deferred binding.
        // Uncomment below to enable dumping the symbol table for debugging.
        symbolTable.debugDumpHtml(output);
        return output.toString();
    }

    /**
     * Take a raw timeline record and convert it into a bottom up profile.
     * 
     * @param rawEvent a raw JSON timeline event of type EventType.PROFILE_DATA
     */
    @Override
    public void parseRawEvent(final JavaScriptProfileEvent rawEvent, final UiEvent refRecord,
            JavaScriptProfile profile) {
        assert rawEvent.getFormat().equals(FORMAT);
        String profileData = rawEvent.getProfileData();
        if (profileData == null || profileData.length() == 0) {
            if (refRecord != null) {
                refRecord.setHasJavaScriptProfile(false);
            }
            return;
        }

        if (workQueue == null) {
            // Process the event synchronously
            currentProfile = profile;
            JSOArray<String> logLines = JSOArray.splitString(rawEvent.getProfileData(), "\n");
            processLogLines(refRecord, logLines, 0);
        } else {
            // Process the log entries using a deferred command to keep from blocking
            // the UI thread.
            if (refRecord != null) {
                refRecord.setProcessingJavaScriptProfile();
            }
            workQueue.append(new NewProfileDataWorker(refRecord, profile, rawEvent));
        }
    }

    /**
     * This method is intended for use by the unit tests only.
     */
    V8Symbol findSymbol(double address) {
        return symbolTable.lookup(address);
    }

    /**
     * Returns a number corresponding to the address string.
     * 
     * @param address a string formatted as a leading + or - followed by a hex
     *          number.
     * 
     * @return a number corresponding to the address string.
     */
    double parseAddress(String addressString, String addressTag) {

        // TODO(zundel): Try using JavaScript's parseInt() rather than
        // Long.parseLong() for better performance. Will performance
        // in Development mode still be acceptable?
        if (addressString.equals("overflow")) {
            return 0;
        } else if (addressString.startsWith("0x")) {
            return Long.parseLong(addressString.substring(2), 16);
        } else if (addressString.startsWith("0")) {
            return Long.parseLong(addressString, 8);
        }

        double baseAddress = 0;
        if (addressTag != null) {
            baseAddress = addressTags.get(addressTag);
        }
        double address = 0;
        if (addressString.startsWith("+")) {
            addressString = addressString.substring(1);
            address = baseAddress + Long.parseLong(addressString, 16);
        } else if (addressString.startsWith("-")) {
            addressString = addressString.substring(1);
            address = baseAddress - Long.parseLong(addressString, 16);
        } else {
            address = Long.parseLong(addressString, 16);
        }
        if (addressTag != null) {
            addressTags.put(addressTag, address);
        }
        return address;
    }

    /**
     * Scrubs a string of any embedded HTML or JavaScript.
     */
    String scrubStringForXSS(String input) {
        scrubbingDiv.setInnerText(input);
        return scrubbingDiv.getInnerText();
    }

    /**
     * Convenience method to create a new ActionType and add it to the map.
     */
    private ActionType createActionType(String actionName, int actionValue) {
        ActionType type = new ActionType(actionName, actionValue);
        actionTypeMap.put(actionName, type);
        return type;
    }

    /**
     * Convenience method to create a new SymbolType and add it to the map.
     */
    private void createSymbolType(String symbolName, int symbolValue) {
        V8SymbolType type = new V8SymbolType(symbolName, symbolValue);
        symbolTypeMap.put(symbolName, type);
    }

    /**
     * Given an array of the fields in a single log line, execute the appropriate
     * action on that entry based on the first field.
     */
    private void parseLogEntry(JsArrayString logEntries) {
        if (logEntries.length() == 0) {
            return;
        }
        String command = logEntries.get(0);
        if (command.length() == 0) {
            return;
        }
        LogAction cmdMethod = logActions.get(actionTypeMap.get(command));
        if (cmdMethod != null) {
            cmdMethod.doAction(logEntries);
        } else if (ClientConfig.isDebugMode()) {
            Logging.getLogger().logText("Unknown v8 profiler command: " + command);
        }
    }

    /**
     * Process an 'alias' command. Simply aliases a command to a different string.
     * The format of this log entry is:
     * 
     * alias, aliasName, originalName
     */
    private void parseV8AliasEntry(JsArrayString logEntries) {
        assert logEntries.length() == 3;

        String originalName = logEntries.get(2);
        String aliasName = logEntries.get(1);

        V8SymbolType symbol = symbolTypeMap.get(originalName);
        if (symbol != null) {
            symbolTypeMap.put(aliasName, symbol);
        } else {
            ActionType action = actionTypeMap.get(originalName);
            if (action != null) {
                actionTypeMap.put(aliasName, action);
            } else if (ClientConfig.isDebugMode()) {
                Logging.getLogger().logText(
                        "Unable to find command: '" + logEntries.get(2) + "' to match alias: " + logEntries.get(1));
            }
        }
    }

    /**
     * New code was added to the virtual machine. The format of this log entry is:
     * 
     * code-creation, symbolType, offset, length, "symbolName"
     * 
     * e.g. code-creation,lic,-5910913e,179,"parentNode"
     * 
     */
    private void parseV8CodeCreationEntry(JsArrayString logEntries) {
        assert logEntries.length() == 5;
        V8SymbolType symbolType = symbolTypeMap.get(logEntries.get(1));

        String name = logEntries.get(4);

        double address = parseAddress(logEntries.get(2), ADDRESS_TAG_CODE);
        int executableSize = Integer.parseInt(logEntries.get(3));

        // Keep some debugging stats around
        V8Symbol found = symbolTable.lookup(address);

        if (found != null) {
            debugStats.addCollisions++;
        }

        V8Symbol symbol = new V8Symbol(scrubStringForXSS(name), symbolType, address, executableSize);

        // We have a heuristic for finding the resource for a Code Creation for
        // functions. The symbol name and function length. It is just a guess, seems
        // to work, and its better than not knowing 100% of the time where a symbol
        // comes from.
        if (symbolType.getValue() == SYMBOL_TYPE_LAZY_COMPILE) {
            lazyCompiledSymbols.put(symbol.getJsSymbol().getSymbolName() + symbol.getAddressSpan().addressLength,
                    symbol);
        }

        if (symbolType.getValue() == SYMBOL_TYPE_FUNCTION) {
            V8Symbol lazySymbol = lazyCompiledSymbols
                    .get(symbol.getJsSymbol().getSymbolName() + symbol.getAddressSpan().addressLength);
            if (lazySymbol != null) {
                symbol.getJsSymbol().merge(lazySymbol.getJsSymbol());
            }
        }

        symbolTable.add(symbol);
    }

    /**
     * Process a code-delete entry in the log.
     * 
     * The format of this entry is:
     * 
     * code-delete, address
     */
    private void parseV8CodeDeleteEntry(JsArrayString logEntries) {
        assert logEntries.length() == 2;
        double address = parseAddress(logEntries.get(1), ADDRESS_TAG_CODE);
        V8Symbol symbol = symbolTable.lookup(address);
        if (symbol != null) {
            symbolTable.remove(symbol);
        } else {
            // update debugging stats
            debugStats.removeMisses++;
        }
    }

    /**
     * Process a code-move entry.
     * 
     * The format of this entry is:
     * 
     * code-move, fromAddress, toAddress
     */
    private void parseV8CodeMoveEntry(JsArrayString logEntries) {
        assert logEntries.length() == 3;
        double fromAddress = parseAddress(logEntries.get(1), ADDRESS_TAG_CODE);
        double toAddress = parseAddress(logEntries.get(2), ADDRESS_TAG_CODE_MOVE);
        V8Symbol symbol = symbolTable.lookup(fromAddress);
        if (symbol != null) {
            symbolTable.remove(symbol);
            symbol.getAddressSpan().setAddress(toAddress);
            symbolTable.add(symbol);
        } else {
            // update debugging stats
            debugStats.moveMisses++;
        }
    }

    /**
     * Parse a profiler entry
     * 
     * The format of this entry is:
     * 
     * profiler, "type", ...
     */
    private void parseV8ProfilerEntry(JsArrayString logEntries) {
        final String arg = logEntries.get(1);
        if (arg.equals("compression")) {
            int windowSize = Integer.parseInt(logEntries.get(2));
            this.logDecompressor = new V8LogDecompressor(windowSize);
        } else if (arg.equals("begin")) {
            // TODO(zundel): make sure all state is reset
            populateAddressTags();
        } else if (arg.equals("pause") || arg.equals("resume")) {
            // ignore pause and resume entries.
        } else if (ClientConfig.isDebugMode()) {
            Logging.getLogger().logText("Ignoring profiler command: " + logEntries.join());
        }
    }

    /**
     * A repeat entry is used to indicate that the command following is repeated
     * multiple times.
     */
    private void parseV8RepeatEntry(JsArrayString logEntries) {
        int numRepeats = Integer.parseInt(logEntries.get(1));
        // run the command after the first 2 arguments numRepeats times.
        logEntries.shift();
        logEntries.shift();
        for (int i = 0; i < numRepeats; ++i) {
            parseLogEntry(logEntries);
        }
    }

    /**
     * Process a tick entry in the v8 log. The format of this log entry is:
     * 
     * command, codeOffset, stackOffset, type, <codeOffset2, <codeOffset3, <...>>>
     * 
     * e.g.: t,-7364bb,+45c,0
     */
    private void parseV8TickEntry(JsArrayString logEntries) {
        assert logEntries.length() >= 4;
        double address = parseAddress(logEntries.get(1), ADDRESS_TAG_CODE);
        // stack address is currently ignored, but it must be parsed to keep the
        // stack address tag up to date if anyone else ever wants to use it.
        // double stackAddress = parseAddress(logEntries.get(2), ADDRESS_TAG_STACK);
        int vmState = Integer.parseInt(logEntries.get(3));
        currentProfile.addStateTime(vmState, 1.0);

        List<V8Symbol> symbols = new ArrayList<V8Symbol>();
        V8Symbol found = symbolTable.lookup(address);
        if (found != null) {
            symbols.add(found);
        }
        addressTags.put(ADDRESS_TAG_SCRATCH, address);
        for (int i = 4; i < logEntries.length(); ++i) {
            address = parseAddress(logEntries.get(i), ADDRESS_TAG_SCRATCH);
            found = symbolTable.lookup(address);
            if (found != null) {
                symbols.add(found);
            }
        }

        updateFlatProfile(symbols, vmState);
        updateBottomUpProfile(symbols, vmState);
        updateTopDownProfile(symbols, vmState);
    }

    private void populateActionTypes() {
        ActionType aliasType = createActionType("alias", ACTION_TYPE_ALIAS);
        ActionType profilerType = createActionType("profiler", ACTION_TYPE_PROFILER);
        ActionType codeCreationType = createActionType("code-creation", ACTION_TYPE_CODE_CREATION);
        ActionType codeMoveType = createActionType("code-move", ACTION_TYPE_CODE_MOVE);
        ActionType codeDeleteType = createActionType("code-delete", ACTION_TYPE_CODE_DELETE);
        ActionType tickType = createActionType("tick", ACTION_TYPE_TICK);
        ActionType repeatType = createActionType("repeat", ACTION_TYPE_REPEAT);

        logActions.put(aliasType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8AliasEntry(logLine);
            }
        });
        logActions.put(profilerType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8ProfilerEntry(logLine);
            }
        });
        logActions.put(codeCreationType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8CodeCreationEntry(logLine);
            }
        });
        logActions.put(codeMoveType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8CodeMoveEntry(logLine);
            }
        });
        logActions.put(codeDeleteType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8CodeDeleteEntry(logLine);
            }
        });
        logActions.put(tickType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8TickEntry(logLine);
            }
        });
        logActions.put(repeatType, new LogAction() {
            public void doAction(JsArrayString logLine) {
                parseV8RepeatEntry(logLine);
            }
        });
    }

    private void populateAddressTags() {
        addressTags.put(ADDRESS_TAG_CODE, 0.);
        addressTags.put(ADDRESS_TAG_CODE_MOVE, 0.);
        addressTags.put(ADDRESS_TAG_STACK, 0.);
    }

    private void populateSymbolTypes() {
        createSymbolType("Builtin", SYMBOL_TYPE_BUILTIN);
        createSymbolType("CallDebugBreak", SYMBOL_TYPE_CALL_DEBUG_BREAK);
        createSymbolType("CallDebugPrepareStepIn", SYMBOL_TYPE_CALL_DEBUG_PREPARE_STEP_IN);
        createSymbolType("CallIC", SYMBOL_TYPE_CALL_IC);
        createSymbolType("CallInitialize", SYMBOL_TYPE_CALL_INITIALIZE);
        createSymbolType("CallMegamorphic", SYMBOL_TYPE_CALL_MEGAMORPHIC);
        createSymbolType("CallMiss", SYMBOL_TYPE_CALL_MISS);
        createSymbolType("CallNormal", SYMBOL_TYPE_CALL_NORMAL);
        createSymbolType("CallPreMonomorphic", SYMBOL_TYPE_PRE_MONOMORPHIC);
        createSymbolType("Callback", SYMBOL_TYPE_CALLBACK);
        createSymbolType("Eval", SYMBOL_TYPE_EVAL);
        createSymbolType("Function", SYMBOL_TYPE_FUNCTION);
        createSymbolType("KeyedCallIC", SYMBOL_TYPE_KEYED_CALL_IC);
        createSymbolType("KeyedLoadIC", SYMBOL_TYPE_KEYED_LOAD_IC);
        createSymbolType("KeyedStoreIC", SYMBOL_TYPE_KEYED_STORE_IC);
        createSymbolType("LazyCompile", SYMBOL_TYPE_LAZY_COMPILE);
        createSymbolType("LoadIC", SYMBOL_TYPE_LOAD_IC);
        createSymbolType("RegExp", SYMBOL_TYPE_REG_EXP);
        createSymbolType("Script", SYMBOL_TYPE_SCRIPT);
        createSymbolType("StoreIC", SYMBOL_TYPE_STORE_IC);
        createSymbolType("Stub", SYMBOL_TYPE_STUB);
    }

    /**
     * Process a portion of the logLines array. If the workQueue is enabled, exit
     * early if the timeslice expires.
     */
    private void processLogLines(final UiEvent refRecord, final JSOArray<String> logLines, int currentLine) {
        final int logLinesLength = logLines.size();

        for (; currentLine < logLinesLength; ++currentLine) {
            if (workQueue != null) {
                // Occasionally check to see if the time to run this chunk has expired.
                if ((currentLine % 10 == 0) && workQueue.isTimeSliceExpired()) {
                    break;
                }
            }

            String logLine = logLines.get(currentLine);
            if (logDecompressor != null && logLine.length() > 0) {
                logLine = logDecompressor.decompressLogEntry(logLine);
            }
            JsArrayString decompressedLogLine = Csv.split(logLine);
            if (decompressedLogLine.length() > 0) {
                parseLogEntry(decompressedLogLine);
            }

            // force gc on processed log lines.
            logLines.set(currentLine, null);
        }

        if (currentLine < logLinesLength) {
            // Schedule this record to be the next thing run off the queue
            workQueue.prepend(new LogLineWorker(logLines, refRecord, currentLine));
        } else {
            // All done!
            if (currentProfile.getProfile(JavaScriptProfile.PROFILE_TYPE_BOTTOM_UP) == null) {
                if (refRecord != null) {
                    refRecord.setHasJavaScriptProfile(false);
                }
            } else {
                if (refRecord != null) {
                    refRecord.setHasJavaScriptProfile(true);
                }
            }
        }
    }

    private JavaScriptProfileNode recordAddressInProfile(JavaScriptProfileNode profileNode, V8Symbol symbol,
            boolean recordedSelfTime) {
        assert profileNode != null;

        JsSymbol jsSymbol = symbol.getJsSymbol();
        JavaScriptProfileNode child = profileNode.lookup(jsSymbol, symbol.getSymbolType().getName());
        if (child == null) {
            child = new JavaScriptProfileNode(jsSymbol, symbol.getSymbolType().getName());
            profileNode.addChild(child);
        }

        if (!recordedSelfTime) {
            child.addSelfTime(1.0);
        } else {
            child.addTime(1.0);
        }
        return child;
    }

    /**
     * No entry has been found in the symbol table. Add or update an unknown
     * entry.
     */
    private void recordUnknownTick(JavaScriptProfileNode profileNode, int vmState) {
        JsSymbol unknownSymbol = new JsSymbol(JavaScriptProfile.NO_RESOURCE, 0,
                "unknown - " + JavaScriptProfile.stateToString(vmState));
        JavaScriptProfileNode child = profileNode.lookup(unknownSymbol, profileNode.getSymbolType());
        if (child == null) {
            child = new JavaScriptProfileNode(unknownSymbol);
            profileNode.addChild(child);
        }
        child.addSelfTime(1.0);
    }

    /**
     * Add this stack frame of resolved symbols to the bottom up profile.
     */
    private void updateBottomUpProfile(List<V8Symbol> symbols, int vmState) {
        JavaScriptProfileNode bottomUpProfile = currentProfile
                .getOrCreateProfile(JavaScriptProfile.PROFILE_TYPE_BOTTOM_UP);
        bottomUpProfile.addTime(1.0);

        if (symbols.size() == 0) {
            recordUnknownTick(bottomUpProfile, vmState);
        } else {
            JavaScriptProfileNode child = bottomUpProfile;
            for (int i = 0, length = symbols.size(); i < length; ++i) {
                child = recordAddressInProfile(child, symbols.get(i), i > 0);
            }
        }
    }

    /**
     * Add this stack frame of resolved symbols to the flat profile.
     */
    private void updateFlatProfile(List<V8Symbol> symbols, int vmState) {
        JavaScriptProfileNode flatProfile = currentProfile.getOrCreateProfile(JavaScriptProfile.PROFILE_TYPE_FLAT);
        flatProfile.addTime(1.0);

        if (symbols.size() == 0) {
            recordUnknownTick(flatProfile, vmState);
        } else {
            Set<V8Symbol> addresses = new HashSet<V8Symbol>();
            for (int i = 0; i < symbols.size(); ++i) {
                V8Symbol symbol = symbols.get(i);
                // We don't want to double-record recursive calls.
                if (!addresses.contains(symbol)) {
                    recordAddressInProfile(flatProfile, symbol, i > 0);
                    addresses.add(symbol);
                }
            }
        }
    }

    /**
     * Add this stack frame of resolved symbols to the top down profile.
     */
    private void updateTopDownProfile(List<V8Symbol> symbols, int vmState) {
        JavaScriptProfileNode topDownProfile = currentProfile
                .getOrCreateProfile(JavaScriptProfile.PROFILE_TYPE_TOP_DOWN);
        topDownProfile.addTime(1.0);

        if (symbols.size() == 0) {
            recordUnknownTick(topDownProfile, vmState);
        } else {
            JavaScriptProfileNode child = topDownProfile;
            Collections.reverse(symbols);
            for (int i = 0, length = symbols.size(); i < length; ++i) {
                child = recordAddressInProfile(child, symbols.get(i), !(i == (symbols.size() - 1)));
            }
        }
    }
}