Re-rolling sourcemap support after rollback. Jar-jared sourcemap.jar dependency and reverted regression in StackTraceCollector.splice().


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10760 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/build.xml b/dev/build.xml
index 7318650..f586b2a 100755
--- a/dev/build.xml
+++ b/dev/build.xml
@@ -59,6 +59,7 @@
           <include name="apache/ant-1.6.5.jar" />
           <include name="eclipse/jdt-3.4.2.jar" />
           <include name="guava/guava-r06/guava-r06-rebased-2.jar" />
+          <include name="jscomp/sourcemap-rebased.jar" />
           <include name="jetty/jetty-6.1.11.jar" />
           <include name="icu4j/4.4.2/icu4j.jar" />
           <include name="protobuf/protobuf-2.2.0/protobuf-java-rebased-2.2.0.jar" />
@@ -111,6 +112,7 @@
           <!-- htmlunit dependencies not already included: END -->
           <include name="sun/swingworker/swing-worker-1.1.jar" />
           <include name="guava/guava-r06/guava-r06-rebased-2.jar" />
+          <include name="jscomp/sourcemap-rebased.jar" />
         </fileset>
         <fileset file="build.xml"/>
       </sourcefiles>
@@ -123,6 +125,7 @@
           <zipfileset src="${gwt.tools.lib}/apache/ant-1.6.5.jar" />
           <zipfileset src="${gwt.tools.lib}/eclipse/jdt-3.4.2.jar" />
           <zipfileset src="${gwt.tools.lib}/guava/guava-r06/guava-r06-rebased-2.jar" />
+          <zipfileset src="${gwt.tools.lib}/jscomp/sourcemap-rebased.jar" />
           <zipfileset src="${gwt.tools.lib}/jetty/jetty-6.1.11.jar" />
           <zipfileset src="${gwt.tools.lib}/icu4j/4.4.2/icu4j.jar" />
           <zipfileset src="${gwt.tools.lib}/protobuf/protobuf-2.2.0/protobuf-java-rebased-2.2.0.jar" />
@@ -175,6 +178,7 @@
           <!-- htmlunit dependencies not already included: END -->
           <zipfileset src="${gwt.tools.lib}/sun/swingworker/swing-worker-1.1.jar" />
           <zipfileset src="${gwt.tools.lib}/guava/guava-r06/guava-r06-rebased-2.jar" />
+          <zipfileset src="${gwt.tools.lib}/jscomp/sourcemap-rebased.jar" />
         </gwt.jar>
       </sequential>
     </outofdate>
@@ -216,6 +220,7 @@
           <pathelement location="${gwt.tools.lib}/eclipse/jdt-3.4.2.jar" />
           <pathelement location="${gwt.tools.lib}/tomcat/commons-collections-3.1.jar" />
           <pathelement location="${gwt.tools.lib}/guava/guava-r06/guava-r06-rebased-2.jar" />
+          <pathelement location="${gwt.tools.lib}/jscomp/sourcemap-rebased.jar" />
       </classpath>
     </gwt.javac>
   </target>
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/SymbolData.java b/dev/core/src/com/google/gwt/core/ext/linker/SymbolData.java
index 97cd1d7..4be3a85 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/SymbolData.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/SymbolData.java
@@ -92,6 +92,11 @@
   String getClassName();
 
   /**
+   * Returns the fragment number in which the symbol is declared (for a method).
+   */
+  int getFragmentNumber();
+
+  /**
    * Returns a JSNI-like identifier for the symbol if it a method or field,
    * otherwise <code>null</code>.
    */
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
index 67d2cdf..64fcc55 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2008 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
@@ -27,6 +27,7 @@
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.core.ext.linker.SoftPermutation;
 import com.google.gwt.core.ext.linker.StatementRanges;
+import com.google.gwt.core.linker.SymbolMapsLinker;
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.TextOutput;
 import com.google.gwt.dev.util.Util;
@@ -46,20 +47,12 @@
 import java.util.TreeMap;
 
 /**
- * A base class for Linkers that use an external script to boostrap the GWT
- * module. This implementation injects JavaScript Snippets into a JS program
- * defined in an external file.
+ * A base class for Linkers that use an external script to boostrap the GWT module. This
+ * implementation injects JavaScript Snippets into a JS program defined in an external file.
  */
 public abstract class SelectionScriptLinker extends AbstractLinker {
-  /**
-   * TODO(bobv): Move this class into c.g.g.core.linker when HostedModeLinker
-   * goes away?
-   */
 
-  /**
-   * A configuration property indicating how large each script tag should be.
-   */
-  private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size";
+  public static final String USE_SOURCE_MAPS_PROPERTY = "compiler.useSourceMaps";
 
   /**
    * File name for computeScriptBase.js.
@@ -87,34 +80,45 @@
   protected static final String PROCESS_METAS_JS = "com/google/gwt/core/ext/linker/impl/processMetasOld.js";
 
   /**
-   * A configuration property that can be used to have the linker load from
-   * somewhere other than {@link #FRAGMENT_SUBDIR}.
+   * TODO(bobv): Move this class into c.g.g.core.linker when HostedModeLinker
+   * goes away?
+   */
+
+  /**
+   * A configuration property indicating how large each script tag should be.
+   */
+  private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size";
+
+  /**
+   * A configuration property that can be used to have the linker load from somewhere other than
+   * {@link #FRAGMENT_SUBDIR}.
    */
   private static final String PROP_FRAGMENT_SUBDIR_OVERRIDE = "iframe.linker.deferredjs.subdir";
 
-  protected static void replaceAll(StringBuffer buf, String search,
-      String replace) {
-    int len = search.length();
-    for (int pos = buf.indexOf(search); pos >= 0; pos = buf.indexOf(search,
-        pos + 1)) {
-      buf.replace(pos, pos + len, replace);
-    }
-  }
-
   /**
-   * Split a JavaScript string into multiple chunks, at statement boundaries.
-   * This method is made default access for testing.
-   * 
-   * @param ranges Describes where the statements are located within the
-   *          JavaScript code. If <code>null</code>, then return <code>js</code>
-   *          unchanged.
-   * @param js The JavaScript code to be split up.
-   * @param charsPerChunk The number of characters to be put in each script tag.
+   * Split a JavaScript string into multiple chunks, at statement boundaries. This method is made
+   * default access for testing.
+   *
+   * @param ranges               Describes where the statements are located within the JavaScript
+   *                             code. If <code>null</code>, then return <code>js</code> unchanged.
+   * @param js                   The JavaScript code to be split up.
+   * @param charsPerChunk        The number of characters to be put in each script tag.
    * @param scriptChunkSeparator The string to insert between chunks.
+   * @param context
    */
   public static String splitPrimaryJavaScript(StatementRanges ranges, String js,
-      int charsPerChunk, String scriptChunkSeparator) {
-    if (charsPerChunk < 0 || ranges == null) {
+      int charsPerChunk, String scriptChunkSeparator, LinkerContext context) {
+    boolean useSourceMaps = false;
+    for (SelectionProperty prop : context.getProperties()) {
+      if (USE_SOURCE_MAPS_PROPERTY.equals(prop.getName())) {
+        String str = prop.tryGetValue();
+        useSourceMaps = str == null ? false : Boolean.parseBoolean(str);
+        break;
+      }
+    }
+
+    // TODO(cromwellian) enable chunking with sourcemaps
+    if (charsPerChunk < 0 || ranges == null || useSourceMaps) {
       return js;
     }
 
@@ -147,13 +151,22 @@
     return sb.toString();
   }
 
+  protected static void replaceAll(StringBuffer buf, String search,
+      String replace) {
+    int len = search.length();
+    for (int pos = buf.indexOf(search); pos >= 0; pos = buf.indexOf(search,
+        pos + 1)) {
+      buf.replace(pos, pos + len, replace);
+    }
+  }
+
   private static char lastChar(StringBuilder sb) {
     return sb.charAt(sb.length() - 1);
   }
 
   /**
-   * This method is left in place for existing subclasses of
-   * SelectionScriptLinker that have not been upgraded for the sharding API.
+   * This method is left in place for existing subclasses of SelectionScriptLinker that have not
+   * been upgraded for the sharding API.
    */
   @Override
   public ArtifactSet link(TreeLogger logger, LinkerContext context,
@@ -169,15 +182,26 @@
       throws UnableToCompleteException {
     if (onePermutation) {
       ArtifactSet toReturn = new ArtifactSet(artifacts);
+      ArtifactSet writableArtifacts = new ArtifactSet(artifacts);
 
       /*
        * Support having multiple compilation results because this method is also
        * called from the legacy link method.
        */
       for (CompilationResult compilation : toReturn.find(CompilationResult.class)) {
-        toReturn.addAll(doEmitCompilation(logger, context, compilation, artifacts));
+        // pass a writable set so that other stages can use this set for temporary storage
+        toReturn.addAll(doEmitCompilation(logger, context, compilation, writableArtifacts));
         maybeAddHostedModeFile(logger, context, toReturn, compilation);
       }
+      /*
+       * Find edit artifacts added during linking and add them. This is done as a way of returning
+       * arbitrary extra outputs from within the linker methods without having to modify
+       * their return signatures to pass extra return data around.
+       */
+      for (SymbolMapsLinker.ScriptFragmentEditsArtifact ea : writableArtifacts.find(
+          SymbolMapsLinker.ScriptFragmentEditsArtifact.class)) {
+        toReturn.add(ea);
+      }
       return toReturn;
     } else {
       permutationsUtil.setupPermutationsMap(artifacts);
@@ -198,8 +222,8 @@
   }
 
   /**
-   * Extract via {@link #CHUNK_SIZE_PROPERTY} the number of characters to be
-   * included in each chunk.
+   * Extract via {@link #CHUNK_SIZE_PROPERTY} the number of characters to be included in each
+   * chunk.
    */
   protected int charsPerChunk(LinkerContext context, TreeLogger logger) {
     SortedSet<ConfigurationProperty> configProps = context.getConfigurationProperties();
@@ -223,9 +247,10 @@
     String[] js = result.getJavaScript();
     byte[][] bytes = new byte[js.length][];
     bytes[0] = generatePrimaryFragment(logger, context, result, js, artifacts);
+
     for (int i = 1; i < js.length; i++) {
-      bytes[i] = Util.getBytes(generateDeferredFragment(logger, context, i,
-          js[i]));
+      bytes[i] = Util.getBytes(generateDeferredFragment(logger, context, i, js[i], artifacts,
+          result));
     }
 
     Collection<Artifact<?>> toReturn = new ArrayList<Artifact<?>>();
@@ -237,7 +262,6 @@
     }
 
     toReturn.addAll(emitSelectionInformation(result.getStrongName(), result));
- 
     return toReturn;
   }
 
@@ -260,10 +284,10 @@
     return emitString(logger, ss, context.getModuleName()
         + ".nocache.js", lastModified);
   }
-  
+
   /**
-   * Generate a selection script. The selection information should previously
-   * have been scanned using {@link PermutationsUtil#setupPermutationsMap(ArtifactSet)}.
+   * Generate a selection script. The selection information should previously have been scanned
+   * using {@link PermutationsUtil#setupPermutationsMap(ArtifactSet)}.
    */
   protected String fillSelectionScriptTemplate(StringBuffer selectionScript,
       TreeLogger logger, LinkerContext context, ArtifactSet artifacts,
@@ -281,10 +305,10 @@
     }
     replaceAll(selectionScript, "__COMPUTE_SCRIPT_BASE__", computeScriptBase);
     replaceAll(selectionScript, "__PROCESS_METAS__", processMetas);
-    
+
     selectionScript = ResourceInjectionUtil.injectResources(selectionScript, artifacts);
     permutationsUtil.addPermutationsJs(selectionScript, logger, context);
-    
+
     replaceAll(selectionScript, "__MODULE_FUNC__",
         context.getModuleFunctionName());
     replaceAll(selectionScript, "__MODULE_NAME__", context.getModuleName());
@@ -292,71 +316,98 @@
 
     return selectionScript.toString();
   }
-  
+
   /**
-   * @param logger a TreeLogger
-   * @param context a LinkerContext
+   * @param logger   a TreeLogger
+   * @param context  a LinkerContext
    * @param fragment the fragment number
    */
   protected String generateDeferredFragment(TreeLogger logger,
-      LinkerContext context, int fragment, String js) {
-    return js;
+      LinkerContext context, int fragment, String js, ArtifactSet artifacts,
+      CompilationResult result)
+      throws UnableToCompleteException {
+    StringBuilder b = new StringBuilder();
+    String strongName = result == null ? "" : result.getStrongName();
+    String prefix = getDeferredFragmentPrefix(logger, context, fragment);
+    b.append(prefix);
+    b.append(js);
+    b.append(getDeferredFragmentSuffix(logger, context, fragment));
+    SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact
+        = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, fragment);
+    editsArtifact.prefixLines(prefix);
+    artifacts.add(editsArtifact);
+    return wrapDeferredFragment(logger, context, fragment, b.toString(), artifacts);
   }
 
   /**
-   * Generate the primary fragment. The default implementation is based on
-   * {@link #getModulePrefix(TreeLogger, LinkerContext, String, int)} and
-   * {@link #getModuleSuffix(TreeLogger, LinkerContext)}.
+   * Generate the primary fragment. The default implementation is based on {@link
+   * #getModulePrefix(TreeLogger, LinkerContext, String, int)} and {@link
+   * #getModuleSuffix(TreeLogger, LinkerContext)}.
    */
   protected byte[] generatePrimaryFragment(TreeLogger logger,
       LinkerContext context, CompilationResult result, String[] js,
       ArtifactSet artifacts) throws UnableToCompleteException {
     TextOutput to = new DefaultTextOutput(context.isOutputCompact());
     String temp = splitPrimaryJavaScript(result.getStatementRanges()[0], js[0],
-        charsPerChunk(context, logger), getScriptChunkSeparator(logger, context));
+        charsPerChunk(context, logger), getScriptChunkSeparator(logger, context), context);
     to.print(generatePrimaryFragmentString(
         logger, context, result, temp, js.length, artifacts));
     return Util.getBytes(to.toString());
   }
-  
+
   protected String generatePrimaryFragmentString(TreeLogger logger,
       LinkerContext context, CompilationResult result, String js, int length,
-      ArtifactSet artifacts) 
-  throws UnableToCompleteException {
+      ArtifactSet artifacts)
+      throws UnableToCompleteException {
     StringBuffer b = new StringBuffer();
     String strongName = result == null ? "" : result.getStrongName();
-    b.append(getModulePrefix(logger, context, strongName, length));
+
+    String modulePrefix = getModulePrefix(logger, context, strongName, length);
+    SymbolMapsLinker.ScriptFragmentEditsArtifact editsArtifact
+        = new SymbolMapsLinker.ScriptFragmentEditsArtifact(strongName, 0);
+    editsArtifact.prefixLines(modulePrefix);
+    artifacts.add(editsArtifact);
+    b.append(modulePrefix);
     b.append(js);
     b.append(getModuleSuffix(logger, context));
     return wrapPrimaryFragment(logger, context, b.toString(), artifacts, result);
   }
-  
+
   protected String generateSelectionScript(TreeLogger logger,
       LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException {
     return generateSelectionScript(logger, context, artifacts, null);
   }
-  
+
   protected String generateSelectionScript(TreeLogger logger,
-        LinkerContext context, ArtifactSet artifacts, CompilationResult result)
-        throws UnableToCompleteException {
+      LinkerContext context, ArtifactSet artifacts, CompilationResult result)
+      throws UnableToCompleteException {
     String selectionScriptText;
     StringBuffer buffer = readFileToStringBuffer(
-        getSelectionScriptTemplate(logger,context), logger);
+        getSelectionScriptTemplate(logger, context), logger);
     selectionScriptText = fillSelectionScriptTemplate(
         buffer, logger, context, artifacts, result);
     selectionScriptText =
-      context.optimizeJavaScript(logger, selectionScriptText);
+        context.optimizeJavaScript(logger, selectionScriptText);
     return selectionScriptText;
   }
-  
+
   protected abstract String getCompilationExtension(TreeLogger logger,
       LinkerContext context) throws UnableToCompleteException;
 
+  protected String getDeferredFragmentPrefix(TreeLogger logger, LinkerContext context,
+      int fragment) {
+    return "";
+  }
+
+  protected String getDeferredFragmentSuffix(TreeLogger logger, LinkerContext context,
+      int fragment) {
+    return "";
+  }
+
   /**
-   * Returns the subdirectory name to be used by getModulPrefix when requesting
-   * a runAsync module. It is specified by
-   * {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is
-   * always {@link #FRAGMENT_SUBDIR}.
+   * Returns the subdirectory name to be used by getModulPrefix when requesting a runAsync module.
+   * It is specified by {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is always
+   * {@link #FRAGMENT_SUBDIR}.
    */
   protected final String getFragmentSubdir(TreeLogger logger,
       LinkerContext context) throws UnableToCompleteException {
@@ -375,27 +426,25 @@
 
     return subdir;
   }
-  
+
   protected String getHostedFilename() {
     return "";
   }
-  
+
   /**
-   * Compute the beginning of a JavaScript file that will hold the main module
-   * implementation.
+   * Compute the beginning of a JavaScript file that will hold the main module implementation.
    */
   protected abstract String getModulePrefix(TreeLogger logger,
       LinkerContext context, String strongName)
       throws UnableToCompleteException;
-  
+
   /**
-   * Compute the beginning of a JavaScript file that will hold the main module
-   * implementation. By default, calls
-   * {@link #getModulePrefix(TreeLogger, LinkerContext, String)}.
-   * 
-   * @param strongName strong name of the module being emitted
-   * @param numFragments the number of fragments for this module, including the
-   *          main fragment (fragment 0)
+   * Compute the beginning of a JavaScript file that will hold the main module implementation. By
+   * default, calls {@link #getModulePrefix(TreeLogger, LinkerContext, String)}.
+   *
+   * @param strongName   strong name of the module being emitted
+   * @param numFragments the number of fragments for this module, including the main fragment
+   *                     (fragment 0)
    */
   protected String getModulePrefix(TreeLogger logger, LinkerContext context,
       String strongName, int numFragments) throws UnableToCompleteException {
@@ -406,9 +455,9 @@
       LinkerContext context) throws UnableToCompleteException;
 
   /**
-   * Some subclasses support "chunking" of the primary fragment. If chunking will
-   * be supported, this function should be overridden to return the string which
-   * should be inserted between each chunk.
+   * Some subclasses support "chunking" of the primary fragment. If chunking will be supported, this
+   * function should be overridden to return the string which should be inserted between each
+   * chunk.
    */
   protected String getScriptChunkSeparator(TreeLogger logger, LinkerContext context) {
     return "";
@@ -416,11 +465,11 @@
 
   protected abstract String getSelectionScriptTemplate(TreeLogger logger,
       LinkerContext context) throws UnableToCompleteException;
-  
+
   /**
    * Add the Development Mode file to the artifact set.
    */
-  protected void maybeAddHostedModeFile(TreeLogger logger, 
+  protected void maybeAddHostedModeFile(TreeLogger logger,
       LinkerContext context, ArtifactSet artifacts, CompilationResult result)
       throws UnableToCompleteException {
     String hostedFilename = getHostedFilename();
@@ -479,6 +528,12 @@
     return buffer;
   }
 
+  protected String wrapDeferredFragment(TreeLogger logger,
+      LinkerContext context, int fragment, String script, ArtifactSet artifacts)
+      throws UnableToCompleteException {
+    return script;
+  }
+
   protected String wrapPrimaryFragment(TreeLogger logger,
       LinkerContext context, String script, ArtifactSet artifacts,
       CompilationResult result) throws UnableToCompleteException {
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSymbolData.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSymbolData.java
index 78cfb0c..d1344a6 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSymbolData.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSymbolData.java
@@ -57,6 +57,7 @@
   }
 
   private String className;
+  private int fragmentNumber = -1;
   private String memberName;
   private String methodSig;
   private int sourceLine;
@@ -91,6 +92,10 @@
     return className;
   }
 
+  public int getFragmentNumber() {
+    return fragmentNumber;
+  }
+
   public String getJsniIdent() {
     if (memberName == null) {
       return null;
@@ -137,6 +142,10 @@
     return methodSig != null;
   }
 
+  public void setFragmentNumber(int fragNum) {
+    fragmentNumber = fragNum;
+  }
+
   public void setSymbolName(String symbolName) {
     this.symbolName = symbolName;
   }
@@ -168,6 +177,7 @@
     queryId = in.readInt();
     castableTypeMap = (CastableTypeMap) in.readObject();
     seedId = in.readInt();
+    fragmentNumber = in.readInt();
   }
 
   /**
@@ -197,5 +207,7 @@
     out.writeInt(queryId);
     out.writeObject(castableTypeMap);
     out.writeInt(seedId);
+    out.writeInt(fragmentNumber);
   }
+
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptAlreadyIncluded.js b/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptAlreadyIncluded.js
index 2417267..d7e56f0 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptAlreadyIncluded.js
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptAlreadyIncluded.js
@@ -8,21 +8,48 @@
   __WAIT_FOR_BODY_LOADED__
 
   function installCode(code) {
-    var docbody = getInstallLocation();
-    for (var i = 0; i < code.length; i++) {
-      var script = getInstallLocationDoc().createElement('script');
-      script.language='javascript';
-      script.text = code[i];
-      docbody.appendChild(script);
-
+     function removeScript(body, element) {
       // Unless we're in pretty mode, remove the tags to shrink the DOM a little.
       // It should have installed its code immediately after being added.
+
       __START_OBFUSCATED_ONLY__
-      docbody.removeChild(script);
+	  body.removeChild(element);
       __END_OBFUSCATED_ONLY__
     }
+
+    var docbody = getInstallLocation();
+    var doc = getInstallLocationDoc();
+    var script;
+    // for sourcemaps, we inject textNodes into the script element on Chrome
+    if (navigator.userAgent.indexOf("Chrome") > -1 && window.JSON) {
+      var scriptFrag = doc.createDocumentFragment()
+      // surround code with eval until crbug #90707 
+      scriptFrag.appendChild(doc.createTextNode("eval(\""));
+      for (var i = 0; i < code.length; i++) {
+        // escape newlines, backslashes, and quotes with JSON.stringify
+        // rather than create multiple script tags which mess up line numbers, we use 1 tag, multiple text nodes
+        var c = window.JSON.stringify(code[i]); 
+        // trim beginning/end quotes
+        scriptFrag.appendChild(doc.createTextNode(c.substring(1, c.length - 1)));
+      }
+      // close the eval
+      scriptFrag.appendChild(doc.createTextNode("\");"));
+      script = doc.createElement('script');
+      script.language='javascript';
+      script.appendChild(scriptFrag);
+      docbody.appendChild(script);
+      removeScript(docbody, script);
+    } else {
+      for (var i = 0; i < code.length; i++) {
+        script = doc.createElement('script');
+ 	script.language='javascript';
+	script.text = code[i];
+        docbody.appendChild(script);
+        removeScript(docbody, script);
+      }
+    }
   }
-  
+
   // Set up a script tag to start downloading immediately, as well as a
   // callback to install the code once it is downloaded and the body is loaded.
   __MODULE_FUNC__.onScriptDownloaded = function(code) {
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptEarlyDownload.js b/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptEarlyDownload.js
index 9375003..52ad608 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptEarlyDownload.js
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/installScriptEarlyDownload.js
@@ -7,21 +7,48 @@
   __WAIT_FOR_BODY_LOADED__
 
   function installCode(code) {
-    var docbody = getInstallLocation();
-    for (var i = 0; i < code.length; i++) {
-      var script = getInstallLocationDoc().createElement('script');
-      script.language='javascript';
-      script.text = code[i];
-      docbody.appendChild(script);
-
+     function removeScript(body, element) {
       // Unless we're in pretty mode, remove the tags to shrink the DOM a little.
       // It should have installed its code immediately after being added.
+
       __START_OBFUSCATED_ONLY__
-      docbody.removeChild(script);
+	  body.removeChild(element);
       __END_OBFUSCATED_ONLY__
     }
+
+    var docbody = getInstallLocation();
+    var doc = getInstallLocationDoc();
+    var script;
+    // for sourcemaps, we inject textNodes into the script element on Chrome
+    if (navigator.userAgent.indexOf("Chrome") > -1 && window.JSON) {
+      var scriptFrag = doc.createDocumentFragment()
+      // surround code with eval until crbug #90707 
+      scriptFrag.appendChild(doc.createTextNode("eval(\""));
+      for (var i = 0; i < code.length; i++) {
+        // escape newlines, backslashes, and quotes with JSON.stringify
+        // rather than create multiple script tags which mess up line numbers, we use 1 tag, multiple text nodes
+        var c = window.JSON.stringify(code[i]); 
+        // trim beginning/end quotes
+        scriptFrag.appendChild(doc.createTextNode(c.substring(1, c.length - 1)));
+      }
+      // close the eval
+      scriptFrag.appendChild(doc.createTextNode("\");"));
+      script = doc.createElement('script');
+      script.language='javascript';
+      script.appendChild(scriptFrag);
+      docbody.appendChild(script);
+      removeScript(docbody, script);
+    } else {
+      for (var i = 0; i < code.length; i++) {
+        script = doc.createElement('script');
+ 	script.language='javascript';
+	script.text = code[i];
+        docbody.appendChild(script);
+        removeScript(docbody, script);
+      }
+    }
   }
-  
+
   // Set up a script tag to start downloading immediately, as well as a
   // callback to install the code once it is downloaded and the body is loaded.
   __MODULE_FUNC__.onScriptDownloaded = function(code) {
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
index f98fe68..9239310 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
@@ -55,7 +55,21 @@
   };
 
   final int end;
+  final int endLine;
+  final int endColumn;
   final int start;
+  final int startLine;
+  final int startColumn;
+
+  /**
+   * Constructor.
+   *
+   * @param start must be non-negative
+   * @param end must be greater than or equal to <code>start</code>
+   */
+  public Range(int start, int end) {
+    this(start, end, 0, 0, 0, 0);
+  }
 
   /**
    * Constructor.
@@ -63,11 +77,17 @@
    * @param start must be non-negative
    * @param end must be greater than or equal to <code>start</code>
    */
-  public Range(int start, int end) {
+  public Range(int start, int end, int startLine, int startColumn, int endLine, int endColumn) {
+
     assert start >= 0;
     assert start <= end;
+
     this.start = start;
     this.end = end;
+    this.startLine = startLine;
+    this.startColumn = startColumn;
+    this.endLine = endLine;
+    this.endColumn = endColumn;
   }
 
   /**
@@ -90,10 +110,26 @@
     return end;
   }
 
+  public int getEndColumn() {
+    return endColumn;
+  }
+
+  public int getEndLine() {
+    return endLine;
+  }
+
   public int getStart() {
     return start;
   }
 
+  public int getStartColumn() {
+    return startColumn;
+  }
+
+  public int getStartLine() {
+    return startLine;
+  }
+
   @Override
   public int hashCode() {
     return 37 * start + end;
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorder.java
new file mode 100644
index 0000000..8bd5bbf
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 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.gwt.core.ext.soyc;
+
+import com.google.gwt.thirdparty.debugging.sourcemap.FilePosition;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapFormat;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGenerator;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorFactory;
+import com.google.gwt.core.ext.linker.SyntheticArtifact;
+import com.google.gwt.core.linker.SymbolMapsLinker;
+import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.SourceInfo;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Creates Closure Compatible SourceMaps.
+ */
+public class SourceMapRecorder {
+
+  public static List<SyntheticArtifact> makeSourceMapArtifacts(
+      List<Map<Range, SourceInfo>> sourceInfoMaps,
+      int permutationId) {
+    List<SyntheticArtifact> toReturn = new ArrayList<SyntheticArtifact>();
+    recordSourceMap(sourceInfoMaps, toReturn, permutationId);
+    return toReturn;
+  }
+
+  public static void recordSourceMap(List<Map<Range, SourceInfo>> sourceInfoMaps,
+       List<SyntheticArtifact> artifacts, int permutationId) {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+    SourceMapGenerator generator = SourceMapGeneratorFactory.getInstance(SourceMapFormat.V3);
+    OutputStreamWriter out = new OutputStreamWriter(baos);
+    int fragment = 0;
+    if (!sourceInfoMaps.isEmpty()) {
+      for (Map<Range, SourceInfo> sourceMap : sourceInfoMaps) {
+        generator.reset();
+        Set<Range> rangeSet = sourceMap.keySet();
+        Range[] ranges = rangeSet.toArray(new Range[rangeSet.size()]);
+        Arrays.sort(ranges, Range.DEPENDENCY_ORDER_COMPARATOR);
+        for (Range r : ranges) {
+          SourceInfo si = sourceMap.get(r);
+          if (si.getFileName() == null || si.getStartLine() < 0) {
+            // skip over synthetics with no Java source
+            continue;
+          }
+          if (r.getStartLine() == 0 || r.getEndLine() == 0) {
+            // or other bogus entries that appear
+            continue;
+          }
+          generator.addMapping(si.getFileName(), null,
+              new FilePosition(si.getStartLine(), 0),
+              new FilePosition(r.getStartLine(), r.getStartColumn()),
+              new FilePosition(r.getEndLine(), r.getEndColumn()));
+        }
+        try {
+          baos.reset();
+          generator.appendTo(out, "sourceMap" + fragment);
+          out.flush();
+          artifacts.add(new SymbolMapsLinker.SourceMapArtifact(permutationId, fragment,
+              baos.toByteArray()));
+          fragment++;
+        } catch (IOException e) {
+          throw new InternalCompilerException(e.toString(), e);
+        }
+      }
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
index 4635518..1c74cf1 100644
--- a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
@@ -61,22 +61,6 @@
   private static final String FAIL_IF_SCRIPT_TAG_PROPERTY = "xsiframe.failIfScriptTag";
 
   @Override
-  protected String generateDeferredFragment(TreeLogger logger,
-      LinkerContext context, int fragment, String js) {
-    // TODO(unnurg): This assumes that the xsiframe linker is using the
-    // ScriptTagLoadingStrategy (since it is also xs compatible).  However,
-    // it should be completely valid to use the XhrLoadingStrategy with this
-    // linker, in which case we would not want to wrap the deferred fragment
-    // in this way.  Ideally, we should make a way for this code to be dependent
-    // on what strategy is being used. Otherwise, we should make a property which
-    // users can set to turn this wrapping off if they override the loading strategy.
-    return String.format("$wnd.%s.runAsyncCallback%d(%s)\n",
-        context.getModuleFunctionName(),
-        fragment,
-        JsToStringGenerationVisitor.javaScriptString(js));
-  }
-  
-  @Override
   public String getDescription() {
     return "Cross-Site-Iframe";
   }
@@ -103,7 +87,7 @@
     // Must do permutations before providers
     includeJs(ss, logger, getJsPermutations(context), "__PERMUTATIONS__");
     includeJs(ss, logger, getJsProperties(context), "__PROPERTIES__");
-    
+
     // Order doesn't matter for the rest
     includeJs(ss, logger, getJsProcessMetas(context), "__PROCESS_METAS__");
     includeJs(ss, logger, getJsInstallLocation(context), "__INSTALL_LOCATION__");
@@ -189,6 +173,11 @@
     return ".cache.js";
   }
 
+   protected String getDeferredFragmentSuffix(TreeLogger logger, LinkerContext context,
+      int fragment) {
+    return "\n//@ sourceURL=" + fragment + ".js\n";
+  }
+
   @Override
   protected String getHostedFilename() {
     return "devmode.js";
@@ -303,7 +292,7 @@
   protected String getJsProperties(LinkerContext context) {
     return "com/google/gwt/core/ext/linker/impl/properties.js";
   }
-  
+
   /**
    * Returns the name of the {@code JsRunAsync} script.  By default,
    * returns {@code "com/google/gwt/core/ext/linker/impl/runAsync.js"}.
@@ -362,7 +351,7 @@
     out.newlineOpt();
     out.print("function __gwtInstallCode(code) {return __gwtModuleFunction.__installRunAsyncCode(code);}");
     out.newlineOpt();
-    
+
     // Even though we call the $sendStats function in the code written in this
     // linker, some of the compilation code still needs the $stats and
     // $sessionId
@@ -386,7 +375,7 @@
         + "__gwtModuleFunction.__computePropValue);");
     out.newlineOpt();
     out.print("$sendStats('moduleStartup', 'end');");
-
+    out.print("\n//@ sourceURL=0.js\n");
     return out.toString();
   }
 
@@ -506,6 +495,22 @@
   }
 
   @Override
+  protected String wrapDeferredFragment(TreeLogger logger,
+      LinkerContext context, int fragment, String js, ArtifactSet artifacts) {
+    // TODO(unnurg): This assumes that the xsiframe linker is using the
+    // ScriptTagLoadingStrategy (since it is also xs compatible).  However,
+    // it should be completely valid to use the XhrLoadingStrategy with this
+    // linker, in which case we would not want to wrap the deferred fragment
+    // in this way.  Ideally, we should make a way for this code to be dependent
+    // on what strategy is being used. Otherwise, we should make a property which
+    // users can set to turn this wrapping off if they override the loading strategy.
+    return String.format("$wnd.%s.runAsyncCallback%d(%s)\n",
+        context.getModuleFunctionName(),
+        fragment,
+        JsToStringGenerationVisitor.javaScriptString(js));
+  }
+
+  @Override
   protected String wrapPrimaryFragment(TreeLogger logger, LinkerContext context, String script,
       ArtifactSet artifacts, CompilationResult result) throws UnableToCompleteException {
     StringBuffer out = new StringBuffer();
@@ -529,5 +534,4 @@
     }
     return out.toString();
   }
-
 }
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index ded6042..fae5c92 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2008 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
@@ -18,15 +18,12 @@
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.linker.ArtifactSet;
-import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.LinkerOrder;
 import com.google.gwt.core.ext.linker.LinkerOrder.Order;
 import com.google.gwt.core.ext.linker.Shardable;
 import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
 import com.google.gwt.dev.About;
 import com.google.gwt.dev.util.DefaultTextOutput;
-import com.google.gwt.dev.util.Util;
 
 /**
  * Implements the canonical GWT bootstrap sequence that loads the GWT module in
@@ -46,26 +43,6 @@
     return "Standard";
   }
 
-  /*
-   * This implementation divides the code of the initial fragment into multiple
-   * script tags. These chunked script tags loads faster on Firefox even when
-   * the data is cached. Additionally, having the script tags separated means
-   * that the early ones can be evaluated before the later ones have finished
-   * downloading. As a result of this parallelism, the overall time to get the
-   * JavaScript downloaded and evaluated can lower.
-   */
-  @Override
-  protected byte[] generatePrimaryFragment(TreeLogger logger,
-      LinkerContext context, CompilationResult result, String[] js,
-      ArtifactSet artifacts) throws UnableToCompleteException {
-    StringBuffer b = new StringBuffer();
-    b.append(getModulePrefix(logger, context, result.getStrongName(), js.length));
-    b.append(splitPrimaryJavaScript(result.getStatementRanges()[0], js[0],
-        charsPerChunk(context, logger), getScriptChunkSeparator(logger, context)));
-    b.append(getModuleSuffix(logger, context));
-    return Util.getBytes(b.toString());
-  }
-  
   @Override
   protected String getCompilationExtension(TreeLogger logger,
       LinkerContext context) {
@@ -118,7 +95,7 @@
   protected String getSelectionScriptTemplate(TreeLogger logger, LinkerContext context) {
     return "com/google/gwt/core/linker/IFrameTemplate.js";
   }
-  
+
   protected String modifyPrimaryJavaScript(String js) {
     return js;
   }
diff --git a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
index 5dc937e..260ed9e 100644
--- a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2009 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
@@ -15,39 +15,176 @@
  */
 package com.google.gwt.core.linker;
 
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorV3;
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.Artifact;
 import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
 import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.LinkerOrder.Order;
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.core.ext.linker.Shardable;
 import com.google.gwt.core.ext.linker.SymbolData;
-import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.core.ext.linker.SyntheticArtifact;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.collect.HashMap;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
 
 /**
- * This Linker exports the symbol maps associated with each compilation result
- * as a private file. The names of the symbol maps files are computed by
- * appending {@value #STRONG_NAME_SUFFIX} to the value returned by
- * {@link CompilationResult#getStrongName()}.
+ * This Linker exports the symbol maps associated with each compilation result as a private file.
+ * The names of the symbol maps files are computed by appending {@value #STRONG_NAME_SUFFIX} to the
+ * value returned by {@link CompilationResult#getStrongName()}.
  */
 @LinkerOrder(Order.POST)
 @Shardable
 public class SymbolMapsLinker extends AbstractLinker {
 
+
   /**
-   * This value is appended to the strong name of the CompilationResult to form
-   * the symbol map's filename.
+   * Artifact to record insertions or deletions made to Javascript fragments.
+   */
+  public static class ScriptFragmentEditsArtifact extends Artifact<ScriptFragmentEditsArtifact> {
+
+    /**
+     * Operation type performed on script.
+     */
+    public enum Edit {
+      PREFIX, INSERT, REMOVE;
+    }
+
+    private static class EditOperation {
+
+      public static EditOperation insert(int lineNumber, String data) {
+        return new EditOperation(Edit.INSERT, lineNumber, data);
+      }
+
+      public static EditOperation prefix(String data) {
+        return new EditOperation(Edit.PREFIX, 0, data);
+      }
+
+      public static EditOperation remove(int lineNumber) {
+        return new EditOperation(Edit.REMOVE, lineNumber, null);
+      }
+
+      Edit op;
+      int lineNumber;
+      int numLines;
+
+      public EditOperation(
+          Edit op, int lineNumber, String data) {
+        this.op = op;
+        this.lineNumber = lineNumber;
+        this.numLines = countNewLines(data);
+      }
+
+      public int getLineNumber() {
+        return lineNumber;
+      }
+
+      public int getNumLines() {
+        return numLines;
+      }
+
+      public Edit getOp() {
+        return op;
+      }
+
+      private int countNewLines(String chunkJs) {
+        int newLineCount = 0;
+        for (int j = 0; j < chunkJs.length(); j++) {
+          if (chunkJs.charAt(j) == '\n') {
+            newLineCount++;
+          }
+        }
+        return newLineCount;
+      }
+    }
+
+    private List<EditOperation> editOperations = new ArrayList<EditOperation>();
+
+    private String strongName;
+    private int fragment;
+
+    public ScriptFragmentEditsArtifact(String strongName,
+        int fragment) {
+      super(SymbolMapsLinker.class);
+      this.strongName = strongName;
+      this.fragment = fragment;
+    }
+
+    public int getFragment() {
+      return fragment;
+    }
+
+    public String getStrongName() {
+      return strongName;
+    }
+
+    @Override
+    public int hashCode() {
+      return (strongName + fragment).hashCode();
+    }
+
+    public void insertLinesBefore(int position, String lines) {
+      editOperations.add(EditOperation.insert(position, lines));
+    }
+
+    public void prefixLines(String lines) {
+      editOperations.add(EditOperation.prefix(lines));
+    }
+
+    @Override
+    protected int compareToComparableArtifact(SymbolMapsLinker.ScriptFragmentEditsArtifact o) {
+      int result = (strongName + fragment).compareTo(strongName + fragment);
+      return result;
+    }
+
+    @Override
+    protected Class<ScriptFragmentEditsArtifact> getComparableArtifactType() {
+      return ScriptFragmentEditsArtifact.class;
+    }
+  }
+
+  /**
+   * Artifact to represent a sourcemap file to be processed by SymbolMapsLinker.
+   */
+  public static class SourceMapArtifact extends SyntheticArtifact {
+
+    private int permutationId;
+    private int fragment;
+    private byte[] js;
+
+    public SourceMapArtifact(int permutationId, int fragment, byte[] js) {
+      super(SymbolMapsLinker.class, permutationId + "/sourceMap" + fragment + ".json", js);
+      this.permutationId = permutationId;
+      this.fragment = fragment;
+      this.js = js;
+    }
+
+    public int getFragment() {
+      return fragment;
+    }
+
+    public int getPermutationId() {
+      return permutationId;
+    }
+  }
+
+  /**
+   * This value is appended to the strong name of the CompilationResult to form the symbol map's
+   * filename.
    */
   public static final String STRONG_NAME_SUFFIX = ".symbolMap";
 
@@ -98,17 +235,59 @@
       throws UnableToCompleteException {
     if (onePermutation) {
       artifacts = new ArtifactSet(artifacts);
-
+      Map<Integer, String> permMap = new HashMap<Integer, String>();
       ByteArrayOutputStream out = new ByteArrayOutputStream();
       for (CompilationResult result : artifacts.find(CompilationResult.class)) {
         PrintWriter pw = new PrintWriter(out);
-
+        permMap.put(result.getPermutationId(), result.getStrongName());
         doWriteSymbolMap(logger, result, pw);
         pw.close();
 
         doEmitSymbolMap(logger, artifacts, result, out);
         out.reset();
       }
+
+      for (SourceMapArtifact se : artifacts.find(SourceMapArtifact.class)) {
+        // filename is permutation_id/sourceMap<fragmentNumber>.json
+        String sourceMapString = Util.readStreamAsString(se.getContents(logger));
+        String strongName = permMap.get(se.getPermutationId());
+        String partialPath = strongName + "_sourceMap" + se.getFragment() + ".json";
+
+        int fragment = se.getFragment();
+        ScriptFragmentEditsArtifact editArtifact = null;
+        for (ScriptFragmentEditsArtifact mp : artifacts.find(ScriptFragmentEditsArtifact.class)) {
+          if (mp.getStrongName().equals(strongName) && mp.getFragment() == fragment) {
+            editArtifact = mp;
+            artifacts.remove(editArtifact);
+            break;
+          }
+        }
+
+        SyntheticArtifact emArt = null;
+        // no need to adjust source map
+        if (editArtifact == null) {
+          emArt = emitSourceMapString(logger, sourceMapString, partialPath);
+        } else {
+          SourceMapGeneratorV3 sourceMapGenerator = new SourceMapGeneratorV3();
+          try {
+            int totalPrefixLines = 0;
+            for (ScriptFragmentEditsArtifact.EditOperation op : editArtifact.editOperations) {
+              if (op.getOp() == ScriptFragmentEditsArtifact.Edit.PREFIX) {
+                totalPrefixLines += op.getNumLines();
+              }
+            }
+            // TODO(cromwellian): apply insert and remove edits
+            sourceMapGenerator.mergeMapSection(totalPrefixLines, 0, sourceMapString);
+            StringWriter stringWriter = new StringWriter();
+            sourceMapGenerator.appendTo(stringWriter, "sourceMap");
+            emArt = emitSourceMapString(logger, stringWriter.toString(), partialPath);
+          } catch (Exception e) {
+            logger.log(TreeLogger.Type.WARN, "Can't write source map " + partialPath, e);
+          }
+        }
+        artifacts.add(emArt);
+        artifacts.remove(se);
+      }
     }
     return artifacts;
   }
@@ -128,11 +307,10 @@
 
   /**
    * Override to change the format of the symbol map.
-   * 
+   *
    * @param logger the logger to write to
    * @param result the compilation result
-   * @param pw the output PrintWriter
-   *
+   * @param pw     the output PrintWriter
    * @throws UnableToCompleteException if an error occurs
    */
   protected void doWriteSymbolMap(TreeLogger logger, CompilationResult result,
@@ -145,7 +323,7 @@
       pw.println(" }");
     }
 
-    pw.println("# jsName, jsniIdent, className, memberName, sourceUri, sourceLine");
+    pw.println("# jsName, jsniIdent, className, memberName, sourceUri, sourceLine, fragmentNumber");
     StringBuilder sb = new StringBuilder(1024);
     char[] buf = new char[1024];
     for (SymbolData symbol : result.getSymbolMap()) {
@@ -170,6 +348,8 @@
       }
       sb.append(',');
       sb.append(symbol.getSourceLine());
+      sb.append(',');
+      sb.append(symbol.getFragmentNumber());
       sb.append('\n');
 
       int sbLen = sb.length();
@@ -185,4 +365,11 @@
       sb.setLength(0);
     }
   }
+
+  protected SyntheticArtifact emitSourceMapString(TreeLogger logger, String contents,
+      String partialPath) throws UnableToCompleteException {
+    SyntheticArtifact emArt = emitString(logger, contents, partialPath);
+    emArt.setVisibility(Visibility.LegacyDeploy);
+    return emArt;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/linker/XSLinker.java b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
index 1ffc1f6..b6b2641 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2008 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
@@ -17,9 +17,10 @@
 
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.core.ext.linker.LinkerOrder;
-import com.google.gwt.core.ext.linker.Shardable;
 import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.core.ext.linker.Shardable;
 import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
 import com.google.gwt.dev.About;
 import com.google.gwt.dev.js.JsToStringGenerationVisitor;
@@ -37,19 +38,6 @@
   }
 
   @Override
-  protected String generateDeferredFragment(TreeLogger logger,
-      LinkerContext context, int fragment, String js) {
-    StringBuilder sb = new StringBuilder();
-    sb.append(context.getModuleFunctionName());
-    sb.append(".runAsyncCallback");
-    sb.append(fragment);
-    sb.append("(");
-    sb.append(JsToStringGenerationVisitor.javaScriptString(js));
-    sb.append(");\n");
-    return sb.toString();
-  }
-
-  @Override
   protected String getCompilationExtension(TreeLogger logger,
       LinkerContext context) {
     return ".cache.js";
@@ -94,6 +82,15 @@
     return "com/google/gwt/core/linker/XSTemplate.js";
   }
 
+   @Override
+  protected String wrapDeferredFragment(TreeLogger logger,
+      LinkerContext context, int fragment, String js, ArtifactSet artifacts) {
+    return String.format("%s.runAsyncCallback%d(%s)\n",
+        context.getModuleFunctionName(),
+        fragment,
+        JsToStringGenerationVisitor.javaScriptString(js));
+  }
+
   private String getModulePrefix(LinkerContext context, String strongName,
       boolean supportRunAsync) {
     DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index db7f4f7..c0bf87c 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -32,6 +32,7 @@
 import com.google.gwt.core.ext.linker.SyntheticArtifact;
 import com.google.gwt.core.ext.linker.impl.StandardSymbolData;
 import com.google.gwt.core.ext.soyc.Range;
+import com.google.gwt.core.ext.soyc.SourceMapRecorder;
 import com.google.gwt.core.ext.soyc.impl.DependencyRecorder;
 import com.google.gwt.core.ext.soyc.impl.SizeMapRecorder;
 import com.google.gwt.core.ext.soyc.impl.SplitPointRecorder;
@@ -120,8 +121,11 @@
 import com.google.gwt.dev.js.JsVerboseNamer;
 import com.google.gwt.dev.js.SizeBreakdown;
 import com.google.gwt.dev.js.ast.JsBlock;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsFunction;
 import com.google.gwt.dev.js.ast.JsName;
 import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsVisitor;
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.Empty;
 import com.google.gwt.dev.util.Memory;
@@ -144,6 +148,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -351,21 +356,11 @@
       }
 
       // detect if browser is ie6 or not known
-      boolean isIE6orUnknown = false;
-      for (PropertyOracle oracle : propertyOracles) {
-        try {
-          SelectionProperty userAgentProperty = oracle.getSelectionProperty(logger, "user.agent");
-          if ("ie6".equals(userAgentProperty.getCurrentValue())) {
-            isIE6orUnknown = true;
-            break;
-          }
-        } catch (BadPropertyValueException e) {
-          // user agent unknown; play it safe
-          isIE6orUnknown = true;
-          break;
-        }
-      }
+      boolean isIE6orUnknown = findBooleanProperty(propertyOracles, logger, "user.agent", "ie6",
+          true, false, true);
 
+      boolean isSourceMapsEnabled = findBooleanProperty(propertyOracles, logger,
+          "compiler.useSourceMaps", "true", true, false, false);
       // (10.5) Obfuscate
       Map<JsName, String> obfuscateMap = Maps.create();
       switch (options.getOutput()) {
@@ -386,7 +381,7 @@
           }
           break;
         case PRETTY:
-          // We don't intern strings in pretty mode to improve readability
+          // We don't intern strings in pretty mode to imprmakeSouove readability
           JsPrettyNamer.exec(jsProgram);
           break;
         case DETAILED:
@@ -421,13 +416,12 @@
       SizeBreakdown[] sizeBreakdowns =
           options.isSoycEnabled() || options.isCompilerMetricsEnabled()
               ? new SizeBreakdown[js.length] : null;
-      List<Map<Range, SourceInfo>> sourceInfoMaps =
-          options.isSoycExtra() ? new ArrayList<Map<Range, SourceInfo>>() : null;
+      List<Map<Range, SourceInfo>> sourceInfoMaps = new ArrayList<Map<Range, SourceInfo>>();
       generateJavaScriptCode(options, jsProgram, jjsmap, js, ranges, sizeBreakdowns,
-          sourceInfoMaps, splitBlocks);
+          sourceInfoMaps, splitBlocks, isSourceMapsEnabled);
 
       PermutationResult toReturn =
-          new PermutationResultImpl(js, permutation, makeSymbolMap(symbolTable), ranges);
+          new PermutationResultImpl(js, permutation, makeSymbolMap(symbolTable, jsProgram), ranges);
       CompilationMetricsArtifact compilationMetrics = null;
       if (options.isCompilerMetricsEnabled()) {
         compilationMetrics = new CompilationMetricsArtifact(permutation.getId());
@@ -441,8 +435,15 @@
             .getPrecompilationMetrics(), compilationMetrics));
       }
       toReturn.addArtifacts(makeSoycArtifacts(logger, permutationId, jprogram, js, sizeBreakdowns,
-          sourceInfoMaps, dependencies, jjsmap, obfuscateMap, unifiedAst.getModuleMetrics(),
-          unifiedAst.getPrecompilationMetrics(), compilationMetrics, options.isSoycHtmlDisabled()));
+          options.isSoycExtra() ? sourceInfoMaps : null, dependencies, jjsmap, obfuscateMap,
+          unifiedAst.getModuleMetrics(), unifiedAst.getPrecompilationMetrics(), compilationMetrics,
+          options.isSoycHtmlDisabled()));
+
+      if (isSourceMapsEnabled) {
+        logger.log(TreeLogger.INFO, "Source Maps Enabled");
+        toReturn.addArtifacts(SourceMapRecorder.makeSourceMapArtifacts(sourceInfoMaps,
+            permutationId));
+      }
 
       logTrackingStats(logger);
       if (logger.isLoggable(TreeLogger.TRACE)) {
@@ -457,6 +458,29 @@
     }
   }
 
+  /**
+   * Look for a selection property in all property oracles.
+   */
+  public static boolean findBooleanProperty(PropertyOracle[] propertyOracles, TreeLogger logger,
+      String name, String valueToFind, boolean valueIfFound, boolean valueIfNotFound,
+      boolean valueIfError) {
+    boolean toReturn = valueIfNotFound;
+    for (PropertyOracle oracle : propertyOracles) {
+      try {
+        SelectionProperty property = oracle.getSelectionProperty(logger, name);
+        if (valueToFind.equals(property.getCurrentValue())) {
+          toReturn = valueIfFound;
+          break;
+        }
+      } catch (BadPropertyValueException e) {
+        // unknown value play it safe
+        toReturn = valueIfError;
+        break;
+      }
+    }
+    return toReturn;
+  }
+
   public static UnifiedAst precompile(TreeLogger logger, ModuleDef module,
       RebindPermutationOracle rpo, String[] declEntryPts, String[] additionalRootTypes,
       JJSOptions options, boolean singlePermutation) throws UnableToCompleteException {
@@ -936,19 +960,22 @@
    * @param options The options this compiler instance is running with
    * @param jsProgram The AST to convert to source code
    * @param jjsMap A map between the JavaScript AST and the Java AST it came
-   *          from
+*          from
    * @param js An array to hold the output JavaScript
    * @param ranges An array to hold the statement ranges for that JavaScript
    * @param sizeBreakdowns An array to hold the size breakdowns for that
-   *          JavaScript
+*          JavaScript
    * @param sourceInfoMaps An array to hold the source info maps for that
-   *          JavaScript
+*          JavaScript
    * @param splitBlocks true if current permutation is for IE6 or unknown
+   * @param sourceMapsEnabled
    */
-  private static void generateJavaScriptCode(JJSOptions options, JsProgram jsProgram,
+  private static void generateJavaScriptCode(JJSOptions options,
+      JsProgram jsProgram,
       JavaToJavaScriptMap jjsMap, String[] js, StatementRanges[] ranges,
-      SizeBreakdown[] sizeBreakdowns, List<Map<Range, SourceInfo>> sourceInfoMaps,
-      boolean splitBlocks) {
+      SizeBreakdown[] sizeBreakdowns,
+      List<Map<Range, SourceInfo>> sourceInfoMaps,
+      boolean splitBlocks, boolean sourceMapsEnabled) {
     for (int i = 0; i < js.length; i++) {
       DefaultTextOutput out = new DefaultTextOutput(options.getOutput().shouldMinimize());
       JsSourceGenerationVisitorWithSizeBreakdown v;
@@ -980,15 +1007,18 @@
        * the top level blocks into sub-blocks if they exceed 32767 statements.
        */
       Event functionClusterEvent = SpeedTracerLogger.start(CompilerEventType.FUNCTION_CLUSTER);
-      // only cluster for obfuscated mode
-      if (options.isAggressivelyOptimize() && options.getOutput() == JsOutputOption.OBFUSCATED) {
+      // TODO(cromwellian) move to the Js AST, re-enable sourcemaps + clustering
+      if (!sourceMapsEnabled
+          && options.isAggressivelyOptimize()
+          // only cluster for obfuscated mode
+          && options.getOutput() == JsOutputOption.OBFUSCATED) {
         transformer = new JsFunctionClusterer(transformer);
         transformer.exec();
       }
       functionClusterEvent.end();
 
       // rewrite top-level blocks to limit the number of statements
-      if (splitBlocks) {
+      if (!sourceMapsEnabled && splitBlocks) {
         transformer = new JsIEBlockTextTransformer(transformer);
         transformer.exec();
       }
@@ -1155,13 +1185,31 @@
     return amp.makeStatement();
   }
 
-  private static SymbolData[] makeSymbolMap(Map<StandardSymbolData, JsName> symbolTable) {
+  private static SymbolData[] makeSymbolMap(Map<StandardSymbolData, JsName> symbolTable,
+      JsProgram jsProgram) {
+
+    final Map<JsName, Integer> nameToFragment = new HashMap<JsName, Integer>();
+    for (int i = 0; i < jsProgram.getFragmentCount(); i++) {
+      final Integer fragId = i;
+      new JsVisitor() {
+        @Override
+        public void endVisit(JsFunction x, JsContext ctx) {
+            if (x.getName() != null) {
+              nameToFragment.put(x.getName(), fragId);
+            }
+        }
+      }.accept(jsProgram.getFragmentBlock(i));
+    }
 
     SymbolData[] result = new SymbolData[symbolTable.size()];
     int i = 0;
     for (Map.Entry<StandardSymbolData, JsName> entry : symbolTable.entrySet()) {
       StandardSymbolData symbolData = entry.getKey();
       symbolData.setSymbolName(entry.getValue().getShortIdent());
+      Integer fragNum = nameToFragment.get(entry.getValue());
+      if (fragNum != null) {
+        symbolData.setFragmentNumber(fragNum);
+      }
       result[i++] = symbolData;
     }
     return result;
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/TextOutputVisitor.java b/dev/core/src/com/google/gwt/dev/jjs/impl/TextOutputVisitor.java
index 0a6f013..d2798ac 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/TextOutputVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/TextOutputVisitor.java
@@ -30,6 +30,16 @@
     this.textOutput = textOutput;
   }
 
+  @Override
+  public int getColumn() {
+    return textOutput.getColumn();
+  }
+
+  @Override
+  public int getLine() {
+    return textOutput.getLine();
+  }
+
   public int getPosition() {
     return textOutput.getPosition();
   }
diff --git a/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
index 58812b1..5d4441b 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
@@ -50,10 +50,13 @@
   protected <T extends JsVisitable> T doAccept(T node) {
     boolean addEntry = node instanceof HasSourceInfo;
     int start = addEntry ? out.getPosition() : 0;
+    int sLine = out.getLine();
+    int sCol = out.getColumn();
     T toReturn = super.doAccept(node);
     if (addEntry) {
       SourceInfo info = ((HasSourceInfo) node).getSourceInfo();
-      sourceInfoMap.put(new Range(start, out.getPosition()), info);
+      sourceInfoMap.put(new Range(start, out.getPosition(),
+          sLine, sCol, out.getLine(), out.getColumn()), info);
     }
     return toReturn;
   }
diff --git a/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java b/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java
index 8e9e8b3..a284d7d 100644
--- a/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java
+++ b/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java
@@ -29,11 +29,21 @@
   private boolean justNewlined;
   private PrintWriter out;
   private int position = 0;
+  private int line = 0;
+  private int column = 0;
 
   protected AbstractTextOutput(boolean compact) {
     this.compact = compact;
   }
 
+  public int getColumn() {
+    return column;
+  }
+
+  public int getLine() {
+    return line;
+  }
+
   public int getPosition() {
     return position;
   }
@@ -63,6 +73,8 @@
       out.print('\n');
     }
     position++;
+    line++;
+    column = 0;
     justNewlined = true;
   }
 
@@ -70,6 +82,8 @@
     if (!compact) {
       out.print('\n');
       position++;
+      line++;
+      column = 0;
       justNewlined = true;
     }
   }
@@ -78,6 +92,7 @@
     maybeIndent();
     out.print(c);
     position++;
+    column++;
     justNewlined = false;
   }
 
@@ -98,6 +113,7 @@
       maybeIndent();
       out.print(c);
       position += 1;
+      column++;
     }
   }
 
@@ -128,6 +144,7 @@
 
   private void printAndCount(char[] chars) {
     position += chars.length;
+    column += chars.length;
     out.print(chars);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/TextOutput.java b/dev/core/src/com/google/gwt/dev/util/TextOutput.java
index ecbf9d0..f98536b 100644
--- a/dev/core/src/com/google/gwt/dev/util/TextOutput.java
+++ b/dev/core/src/com/google/gwt/dev/util/TextOutput.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2006 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
@@ -19,6 +19,11 @@
  * Interface used for printing text output.
  */
 public interface TextOutput {
+
+  int getColumn();
+
+  int getLine();
+
   int getPosition();
 
   void indentIn();
diff --git a/dev/core/test/com/google/gwt/core/linker/ScriptChunkingTest.java b/dev/core/test/com/google/gwt/core/linker/ScriptChunkingTest.java
index 266935d..f5e0e6b 100644
--- a/dev/core/test/com/google/gwt/core/linker/ScriptChunkingTest.java
+++ b/dev/core/test/com/google/gwt/core/linker/ScriptChunkingTest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2009 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
@@ -15,6 +15,11 @@
  */
 package com.google.gwt.core.linker;
 
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.core.ext.linker.StatementRanges;
 import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
 import com.google.gwt.core.ext.linker.impl.StandardStatementRanges;
@@ -22,16 +27,92 @@
 import junit.framework.TestCase;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 /**
  * Tests the script chunking in the {@link SelectionScriptLinker}.
  */
 public class ScriptChunkingTest extends TestCase {
+
+  private static class MockLinkerContext implements LinkerContext {
+
+    @Override
+    public SortedSet<ConfigurationProperty> getConfigurationProperties() {
+      return new TreeSet<ConfigurationProperty>();
+    }
+
+    @Override
+    public String getModuleFunctionName() {
+      return "mockFunc";
+    }
+
+    @Override
+    public long getModuleLastModified() {
+      return 0;
+    }
+
+    @Override
+    public String getModuleName() {
+      return "mock";
+    }
+
+    @Override
+    public SortedSet<SelectionProperty> getProperties() {
+      SelectionProperty mockSourceMapProperty = new SelectionProperty() {
+
+        @Override
+        public String getName() {
+          return SelectionScriptLinker.USE_SOURCE_MAPS_PROPERTY;
+        }
+
+        @Override
+        public String getFallbackValue() {
+          return "";
+        }
+
+        @Override
+        public SortedSet<String> getPossibleValues() {
+          return new TreeSet<String>(Arrays.asList("true", "false"));
+        }
+
+        @Override
+        public String getPropertyProvider(TreeLogger logger,
+            SortedSet<ConfigurationProperty> configProperties) throws UnableToCompleteException {
+          return null;
+        }
+
+        @Override
+        public boolean isDerived() {
+          return false;
+        }
+
+        @Override
+        public String tryGetValue() {
+          return "false";
+        }
+      };
+      return new TreeSet<SelectionProperty>(Arrays.asList(mockSourceMapProperty));
+    }
+
+    @Override
+    public boolean isOutputCompact() {
+      return false;
+    }
+
+    @Override
+    public String optimizeJavaScript(TreeLogger logger, String jsProgram)
+        throws UnableToCompleteException {
+      return "";
+    }
+  }
+
   /**
-   * A class for building up JavaScript that has statements and non-statement
-   * code interleaved.
+   * A class for building up JavaScript that has statements and non-statement code interleaved.
    */
   private static class ScriptWithRangesBuilder {
+
     private final ArrayList<Integer> ends = new ArrayList<Integer>();
     private final StringBuffer script = new StringBuffer();
     private final ArrayList<Integer> starts = new ArrayList<Integer>();
@@ -74,12 +155,12 @@
 
     String split = SelectionScriptLinker.splitPrimaryJavaScript(builder.getRanges(),
         builder.getJavaScript(), stmt1.length() + stmt2.length(),
-        IFrameLinker.SCRIPT_CHUNK_SEPARATOR);
+        IFrameLinker.SCRIPT_CHUNK_SEPARATOR, new MockLinkerContext());
     assertEquals(stmt1 + stmt2 + IFrameLinker.SCRIPT_CHUNK_SEPARATOR + stmt3
         + ';' + stmt4, split);
   }
 
-  
+
   /**
    * Test that with the default chunk separator (""), splitting is a no-op.
    */
@@ -99,10 +180,10 @@
     builder.addNonStatement("}");
 
     String split = SelectionScriptLinker.splitPrimaryJavaScript(builder.getRanges(),
-        builder.getJavaScript(), stmt1.length() + stmt2.length(), "");
+        builder.getJavaScript(), stmt1.length() + stmt2.length(), "", new MockLinkerContext());
     assertEquals(stmt1 + stmt2 + stmt3 + ';' + stmt4, split);
   }
-  
+
   /**
    * Test a chunk size large enough that no splitting happens.
    */
@@ -122,7 +203,8 @@
     builder.addNonStatement("}");
 
     String split = SelectionScriptLinker.splitPrimaryJavaScript(builder.getRanges(),
-        builder.getJavaScript(), 10000, IFrameLinker.SCRIPT_CHUNK_SEPARATOR);
+        builder.getJavaScript(), 10000, IFrameLinker.SCRIPT_CHUNK_SEPARATOR,
+        new MockLinkerContext());
     assertEquals(stmt1 + stmt2 + stmt3 + ';' + stmt4, split);
   }
 
@@ -141,7 +223,7 @@
     builder.addNonStatement("}");
 
     String split = SelectionScriptLinker.splitPrimaryJavaScript(builder.getRanges(),
-        builder.getJavaScript(), -1, IFrameLinker.SCRIPT_CHUNK_SEPARATOR);
+        builder.getJavaScript(), -1, IFrameLinker.SCRIPT_CHUNK_SEPARATOR, new MockLinkerContext());
     assertEquals(builder.getJavaScript(), split);
   }
 
@@ -160,7 +242,7 @@
     builder.addNonStatement("}");
 
     String split = SelectionScriptLinker.splitPrimaryJavaScript(null,
-        builder.getJavaScript(), 5, IFrameLinker.SCRIPT_CHUNK_SEPARATOR);
+        builder.getJavaScript(), 5, IFrameLinker.SCRIPT_CHUNK_SEPARATOR, new MockLinkerContext());
     assertEquals(builder.getJavaScript(), split);
   }
 }
diff --git a/servlet/build.xml b/servlet/build.xml
index f274e05..21a57be 100755
--- a/servlet/build.xml
+++ b/servlet/build.xml
@@ -52,6 +52,7 @@
       <!-- The following two jars satisfy RequestFactory dependencies. -->
       <zipfileset src="${gwt.tools.redist}/json/r2_20080312/json-1.5.jar" />
       <zipfileset src="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA.jar" />
+      <zipfileset src="${gwt.tools.lib}/jscomp/sourcemap-rebased.jar" />
     </gwt.jar>
   </target>
 </project>
diff --git a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
index b811b41..a3b11ef 100644
--- a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
+++ b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
@@ -75,4 +75,10 @@
     is-multi-valued="false" />
   <set-configuration-property name="iframe.linker.deferredjs.subdir"
     value="deferredjs" />
+
+  <!--
+     Specifies whether a given permutation has source map support enabled.
+  -->
+  <define-property name="compiler.useSourceMaps" values="true,false"/>
+  <set-property name="compiler.useSourceMaps" value="false"/>
 </module>
diff --git a/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml b/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
index c4cbd6c..a829874 100644
--- a/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
+++ b/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
@@ -43,4 +43,26 @@
     <when-type-is class="com.google.gwt.core.client.impl.StackTraceCreator.Collector" />
     <when-property-is name="compiler.stackMode" value="strip" />
   </replace-with>
+
+  <!-- Disable source maps for non-Chrome browsers -->
+  <set-property name="compiler.useSourceMaps" value="false">
+    <none> 
+      <when-property-is name="user.agent" value="safari"/>
+    </none> 
+  </set-property>
+
+<set-property name="compiler.useSourceMaps" value="false"/>
+
+  <!-- Utility class to query if source maps are enabled, mainly for testing -->
+  <replace-with class="com.google.gwt.core.client.impl.SourceMapProperty.SourceMapEnabled">
+    <when-type-is class="com.google.gwt.core.client.impl.SourceMapProperty.SourceMapImpl"/>
+    <when-property-is name="compiler.useSourceMaps" value="true"/>
+  </replace-with>
+
+  <!-- If stack trace emulation is on, we can still get accurate stack traces even if sourcemaps are off -->
+  <replace-with class="com.google.gwt.core.client.impl.SourceMapProperty.SourceMapEnabled">
+    <when-type-is class="com.google.gwt.core.client.impl.SourceMapProperty.SourceMapImpl"/>
+    <when-property-is name="compiler.useSourceMaps" value="false"/>
+    <when-property-is name="compiler.stackMode" value="emulated"/>
+  </replace-with>
 </module>
diff --git a/user/src/com/google/gwt/core/client/impl/SourceMapProperty.java b/user/src/com/google/gwt/core/client/impl/SourceMapProperty.java
new file mode 100644
index 0000000..6e79367
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/impl/SourceMapProperty.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2011 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.gwt.core.client.impl;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * Helper class to query if SourceMaps are enabled and capable of working on the current user
+ * agent.
+ */
+public class SourceMapProperty {
+
+  static class SourceMapEnabled extends SourceMapImpl {
+
+    public native boolean doesBrowserSupportSourceMaps() /*-{
+      // Chrome only for now, future Firefoxes have promised support
+      return navigator.userAgent.indexOf('Chrome') > -1;
+    }-*/;
+
+    public boolean isSourceMapGenerationOn() {
+      return true;
+    }
+  }
+
+  static class SourceMapEmulated extends SourceMapEnabled {
+
+    public boolean shouldUseSourceMaps() {
+      // pretend emulated stack is a sourcemap
+      return true;
+    }
+  }
+
+  /**
+   * Interface to provide both the compile time and runtime <code>user.agent</code> selection
+   * property value.
+   */
+  static class SourceMapImpl {
+
+    public boolean doesBrowserSupportSourceMaps() {
+      return false;
+    }
+
+    public boolean isSourceMapGenerationOn() {
+      return false;
+    }
+
+    public boolean shouldUseSourceMaps() {
+      return isSourceMapGenerationOn() && doesBrowserSupportSourceMaps();
+    }
+  }
+
+  private static final SourceMapImpl IMPL = GWT.create(SourceMapImpl.class);
+
+  public static boolean doesBrowserSupportSourceMaps() {
+    return IMPL.doesBrowserSupportSourceMaps();
+  }
+
+  /**
+   * True if fully accurate stack traces are possible. True for DevMode, emulated stack traces, and
+   * cases where sourceMaps can work with detailed browser stack trace support.
+   */
+  public static boolean isDetailedDeobfuscatedStackTraceSupported() {
+    return !GWT.isScript() || shouldUseSourceMaps();
+  }
+
+  public static boolean isSourceMapGenerationOn() {
+    return IMPL.isSourceMapGenerationOn();
+  }
+
+  public static boolean shouldUseSourceMaps() {
+    return IMPL.shouldUseSourceMaps();
+  }
+}
diff --git a/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java b/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
index 3aa6182..2a73797 100644
--- a/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
+++ b/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2009 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
@@ -26,6 +26,19 @@
  * Production Mode.
  */
 public class StackTraceCreator {
+
+  /**
+   * Line number used in a stack trace when it is unknown.
+   */
+  public static final int LINE_NUMBER_UNKNOWN = -1;
+
+  /**
+   * Used to encode a (column, line) pair into a line number (since LogRecord only stores line
+   * numbers). We store line, col as line + col * max_line, and use division and modulus to
+   * recover on the server side.
+   */
+  public static final int MAX_LINE_NUMBER = 100000;
+
   /**
    * This class acts as a deferred-binding hook point to allow more optimal
    * versions to be substituted. This base version simply crawls
@@ -107,7 +120,7 @@
     /**
      * Attempt to infer the stack from an unknown JavaScriptObject that had been
      * thrown. The default implementation just returns an empty array.
-     * 
+     *
      * @param e a JavaScriptObject
      */
     public JsArrayString inferFrom(JavaScriptObject e) {
@@ -245,12 +258,12 @@
 
   /**
    * Chrome uses a slightly different format to Mozilla.
-   * 
+   *
    * See http://code.google.com/p/v8/source/browse/branches/bleeding_edge/src/
    * messages.js?r=2340#712 for formatting code.
-   * 
+   *
    * Function calls can be of the four following forms:
-   * 
+   *
    * <pre>
    * at file.js:1:2
    * at functionName (file.js:1:2)
@@ -259,6 +272,7 @@
    * </pre>
    */
   static class CollectorChrome extends CollectorMoz {
+
     @Override
     public JsArrayString collect() {
       JsArrayString res = super.collect();
@@ -273,6 +287,18 @@
     }
 
     @Override
+    public void createStackTrace(JavaScriptException e) {
+      JsArrayString stack = inferFrom(e.getException());
+      parseStackTrace(e, stack);
+    }
+
+    @Override
+    public void fillInStackTrace(Throwable t) {
+      JsArrayString stack = StackTraceCreator.createStackTrace();
+      parseStackTrace(t, stack);
+    }
+
+    @Override
     public JsArrayString inferFrom(JavaScriptObject e) {
       JsArrayString stack = super.inferFrom(e);
       if (stack.length() == 0) {
@@ -286,8 +312,11 @@
 
     @Override
     protected String extractName(String fnToString) {
+      String extractedName = "anonymous";
+      String location = "";
+
       if (fnToString.length() == 0) {
-        return "anonymous";
+        return extractedName;
       }
 
       String toReturn = fnToString.trim();
@@ -297,34 +326,70 @@
         toReturn = toReturn.substring(3);
       }
 
-      // Strip bracketed items from the end:
+      // Strip square bracketed items from the end:
       int index = toReturn.indexOf("[");
-      if (index == -1) {
-        index = toReturn.indexOf("(");
+      if (index != -1) {
+        toReturn = toReturn.substring(0, index).trim() +
+            toReturn.substring(toReturn.indexOf("]", index) + 1).trim();
       }
+
+      index = toReturn.indexOf("(");
       if (index == -1) {
-        // No bracketed items found, hence no function name available:
-        return "anonymous";
+        // No bracketed items found, hence no function name available
+        location = toReturn;
+        toReturn = "";
       } else {
-        // Bracketed items found: strip them off.
+        // Bracketed items found: strip them off, parse location info
+        int closeParen = toReturn.indexOf(")", index);
+        location = toReturn.substring(index + 1, closeParen);
         toReturn = toReturn.substring(0, index).trim();
       }
 
-      // Strip the Type off to leave just the functionName:
+      // Strip the Type off t
       index = toReturn.indexOf('.');
       if (index != -1) {
         toReturn = toReturn.substring(index + 1);
       }
-
-      return toReturn.length() > 0 ? toReturn : "anonymous";
+      return (toReturn.length() > 0 ? toReturn : "anonymous") + "@@" + location;
     }
 
     @Override
     protected int toSplice() {
       return 3;
     }
+
+    private void parseStackTrace(Throwable e, JsArrayString stack) {
+      StackTraceElement[] stackTrace = new StackTraceElement[stack.length()];
+      for (int i = 0, j = stackTrace.length; i < j; i++) {
+        String stackElements[] = stack.get(i).split("@@");
+
+        int line = LINE_NUMBER_UNKNOWN;
+        int col = -1;
+        String fileName = "Unknown";
+        if (stackElements.length == 2 && stackElements[1] != null) {
+          String location = stackElements[1];
+          // colon between line and column
+          int lastColon = location.lastIndexOf(':');
+          // colon between file url and line number
+          int endFileUrl = location.lastIndexOf(':', lastColon - 1);
+          fileName = location.substring(0, endFileUrl);
+
+          if (lastColon != -1 && endFileUrl != -1) {
+              line = parseInt(location.substring(endFileUrl + 1, lastColon));
+              col = parseInt(location.substring(lastColon + 1));
+          }
+        }
+        stackTrace[i] = new StackTraceElement("Unknown", stackElements[0], fileName,
+            line < 0 ? -1 : col * MAX_LINE_NUMBER + line);
+      }
+      e.setStackTrace(stackTrace);
+    }
   }
 
+  private static native int parseInt(String number) /*-{
+    return parseInt(number) || -1;
+  }-*/;
+
   /**
    * Opera encodes stack trace information in the error's message.
    */
@@ -392,8 +457,6 @@
       // empty, since Throwable.getStackTrace() properly handles null
     }
   }
-  
-  private static final int LINE_NUMBER_UNKNOWN = -1;
 
   /**
    * Create a stack trace based on a JavaScriptException. This method should
diff --git a/user/src/com/google/gwt/junit/JUnit.gwt.xml b/user/src/com/google/gwt/junit/JUnit.gwt.xml
index 29cdbc4..0e30089 100644
--- a/user/src/com/google/gwt/junit/JUnit.gwt.xml
+++ b/user/src/com/google/gwt/junit/JUnit.gwt.xml
@@ -34,7 +34,12 @@
 
   <!-- We want to provide consistent stack traces across all browsers. -->
   <set-configuration-property name="compiler.emulatedStack.recordFileNames" value="true" />
-  <set-property name="compiler.stackMode" value="emulated" />
+  <set-property name="compiler.stackMode" value="emulated">
+    <none>
+      <!-- Don't use stack trace emulation if source maps enabled -->
+      <when-property-is name="compiler.useSourceMaps" value="true"/>
+    </none>
+  </set-property>
 
   <!-- Override the regular symbolMaps linker to put the data somewhere we can find it -->
   <define-linker name="symbolMaps" class="com.google.gwt.junit.linker.JUnitSymbolMapsLinker" />
diff --git a/user/src/com/google/gwt/junit/linker/JUnitSymbolMapsLinker.java b/user/src/com/google/gwt/junit/linker/JUnitSymbolMapsLinker.java
index 723654c..4638ec0 100644
--- a/user/src/com/google/gwt/junit/linker/JUnitSymbolMapsLinker.java
+++ b/user/src/com/google/gwt/junit/linker/JUnitSymbolMapsLinker.java
@@ -21,6 +21,7 @@
 import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.Shardable;
+import com.google.gwt.core.ext.linker.SyntheticArtifact;
 import com.google.gwt.core.linker.SymbolMapsLinker;
 
 import java.io.ByteArrayOutputStream;
@@ -31,12 +32,15 @@
  */
 @Shardable
 public class JUnitSymbolMapsLinker extends SymbolMapsLinker {
+
+  public static final String SYMBOL_MAP_DIR = ".junit_symbolMaps/";
+
   @Override
   protected void doEmitSymbolMap(TreeLogger logger, ArtifactSet artifacts,
       CompilationResult result, ByteArrayOutputStream out)
       throws UnableToCompleteException {
     // Collaborate with JUnitHostImpl.loadSymbolMap
-    String partialPath = ".junit_symbolMaps/" + result.getStrongName()
+    String partialPath = SYMBOL_MAP_DIR + result.getStrongName()
         + STRONG_NAME_SUFFIX;
 
     EmittedArtifact symbolMapArtifact = emitBytes(logger, out.toByteArray(),
@@ -45,4 +49,9 @@
     artifacts.add(symbolMapArtifact);
   }
 
+  @Override
+  protected SyntheticArtifact emitSourceMapString(TreeLogger logger, String contents,
+      String partialPath) throws UnableToCompleteException {
+    return emitString(logger, contents, SYMBOL_MAP_DIR + partialPath);
+  }
 }
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 9f79b68..ef26877 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -233,7 +233,7 @@
       if (symbolData != null) {
         // jsniIdent, className, memberName, sourceUri, sourceLine
         String[] parts = symbolData.split(",");
-        assert parts.length == 5 : "Expected 5, have " + parts.length;
+        assert parts.length == 6 : "Expected 6, have " + parts.length;
 
         JsniRef ref = JsniRef.parse(parts[0].substring(0,
             parts[0].lastIndexOf(')') + 1));
diff --git a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
index b8f7dfd..adb20bc 100644
--- a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
+++ b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
@@ -71,12 +71,16 @@
     Throwable cause =
       throwableFromJson(t.getString("cause"));
     StackTraceElement[] stackTrace = null;
-    JSONArray st = t.getJSONArray("stackTrace");
-    if (st.length() > 0) {
-      stackTrace = new StackTraceElement[st.length()];
-      for (int i = 0; i < st.length(); i++) {
-        stackTrace[i] = stackTraceElementFromJson(st.getString(i));
+    if (t.has("stackTrace")) {
+      JSONArray st = t.getJSONArray("stackTrace");
+      if (st.length() > 0) {
+        stackTrace = new StackTraceElement[st.length()];
+        for (int i = 0; i < st.length(); i++) {
+          stackTrace[i] = stackTraceElementFromJson(st.getString(i));
+        }
       }
+    } else {
+      stackTrace = new StackTraceElement[0];
     }
     Throwable thrown = new Throwable(message, cause);
     thrown.setStackTrace(stackTrace);
diff --git a/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java b/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java
index 472afaf..545b6c4 100644
--- a/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java
+++ b/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,6 +16,14 @@
 
 package com.google.gwt.logging.server;
 
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerFactory;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerV3;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapping;
+import com.google.gwt.thirdparty.debugging.sourcemap.proto.Mapping;
+import com.google.gwt.core.client.impl.StackTraceCreator;
+
+import org.json.JSONObject;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
@@ -24,62 +32,70 @@
 import java.io.InputStreamReader;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Scanner;
 import java.util.logging.LogRecord;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * Deobfuscates stack traces on the server side. This class requires that you
- * have turned on emulated stack traces, via
- * <code>&lt;set-property name="compiler.stackMode" value="emulated" /&gt;</code>
- * in your <code>.gwt.xml</code> module file, and moved your symbol map files to
- * a location accessible by your server sever side code. You can use the GWT
- * compiler <code>-deploy</code> command line argument to specify the location
- * of the folder into which the generated <code>symbolMaps</code> directory is
- * written. By default, the final <code>symbolMaps</code> directory is
- * <code>war/WEB-INF/deploy/<i>yourmodulename</i>/symbolMaps/</code>. Pass the
- * resulting directory location into this class'
- * {@link StackTraceDeobfuscator#symbolMapsDirectory} constructor or
- * {@link #setSymbolMapsDirectory(String)} setter method.
- * 
+ * Deobfuscates stack traces on the server side. This class requires that you have turned on
+ * emulated stack traces, via <code>&lt;set-property name="compiler.stackMode" value="emulated"
+ * /&gt;</code> in your <code>.gwt.xml</code> module file for non-Chrome browsers or
+ * <code>&lt;set-property name="compiler.useSourceMaps" value="true"/&gt;</code> for Chrome, and
+ * moved your symbol map files to a location accessible by your server sever side code. You can use
+ * the GWT compiler <code>-deploy</code> command line argument to specify the location of the folder
+ * into which the generated <code>symbolMaps</code> directory is written. By default, the final
+ * <code>symbolMaps</code> directory is <code>war/WEB-INF/deploy/<i>yourmodulename</i>/symbolMaps/</code>.
+ * Pass the resulting directory location into this class' {@link StackTraceDeobfuscator#symbolMapsDirectory}
+ * constructor or {@link #setSymbolMapsDirectory(String)} setter method.
+ *
  * TODO(unnurg): Combine this code with similar code in JUnitHostImpl
  */
 public class StackTraceDeobfuscator {
-  
-  private static class SymbolMap extends HashMap<String, String> { }
-  
+
+  private static class SymbolMap extends HashMap<String, String> {
+  }
+
   // From JsniRef class, which is in gwt-dev and so can't be accessed here
   // TODO(unnurg) once there is a place for shared code, move this to there.
   private static Pattern JsniRefPattern =
-    Pattern.compile("@?([^:]+)::([^(]+)(\\((.*)\\))?");
-  
-  // The javadoc for StackTraceElement.getLineNumber() says it returns -1 when 
+      Pattern.compile("@?([^:]+)::([^(]+)(\\((.*)\\))?");
+
+  // The javadoc for StackTraceElement.getLineNumber() says it returns -1 when
   // the line number is unavailable
   private static final int LINE_NUMBER_UNKNOWN = -1;
-  
-  private File symbolMapsDirectory;
-  
+
+  Pattern fragmentIdPattern = Pattern.compile(".*(\\d+)\\.js");
+
+  protected File symbolMapsDirectory;
+
+  // Map of strongName + fragmentId to sourceMap
+  private Map<String, SourceMapping> sourceMaps =
+      new HashMap<String, SourceMapping>();
+
+  private Map<String, Integer> sourceMapPrefixes = new HashMap<String, Integer>();
+
   private Map<String, SymbolMap> symbolMaps =
-    new HashMap<String, SymbolMap>();
+      new HashMap<String, SymbolMap>();
 
   /**
-   * Constructor, which takes a <code>symbolMaps</code> directory as its
-   * argument. Symbol maps are generated into the location specified by the
-   * GWT compiler <code>-deploy</code> command line argument.
-   * 
-   * @param symbolMapsDirectory the <code>symbolMaps</code> directory with, or
-   *          without trailing directory separator character
+   * Constructor, which takes a <code>symbolMaps</code> directory as its argument. Symbol maps are
+   * generated into the location specified by the GWT compiler <code>-deploy</code> command line
+   * argument.
+   *
+   * @param symbolMapsDirectory the <code>symbolMaps</code> directory with, or without trailing
+   *                            directory separator character
    */
   public StackTraceDeobfuscator(String symbolMapsDirectory) {
     setSymbolMapsDirectory(symbolMapsDirectory);
   }
-  
+
   /**
    * Best effort resymbolization of a log record's stack trace.
-   * 
-   * @param lr the log record to resymbolize
+   *
+   * @param lr         the log record to resymbolize
    * @param strongName the GWT permutation strong name
-   * @return the best effort resymbolized log record
+   * @return the best effort resymbolized log record                                   JJSO
    */
   public LogRecord deobfuscateLogRecord(LogRecord lr, String strongName) {
     if (lr.getThrown() != null && strongName != null) {
@@ -87,12 +103,11 @@
     }
     return lr;
   }
-  
+
   /**
-   * Convenience method which resymbolizes an entire stack trace to extent
-   * possible.
+   * Convenience method which resymbolizes an entire stack trace to extent possible.
    *
-   * @param st the stack trace to resymbolize
+   * @param st         the stack trace to resymbolize
    * @param strongName the GWT permutation strong name
    * @return a best effort resymbolized stack trace
    */
@@ -104,28 +119,34 @@
     }
     return newSt;
   }
-  
+
   /**
    * Best effort resymbolization of a a single stack trace element.
    *
-   * @param ste the stack trace element to resymbolize
+   * @param ste        the stack trace element to resymbolize
    * @param strongName the GWT permutation strong name
    * @return the best effort resymbolized stack trace element
    */
   public StackTraceElement resymbolize(StackTraceElement ste,
       String strongName) {
+    String declaringClass = null;
+    String methodName = null;
+    String filename = null;
+    int lineNumber = -1;
+    int fragmentId = -1;
+
+    String steFilename = ste.getFileName();
     SymbolMap map = loadSymbolMap(strongName);
     String symbolData = map == null ? null : map.get(ste.getMethodName());
 
+    // first use symbolMap, then refine via sourceMap if possible
     if (symbolData != null) {
-      // jsniIdent, className, memberName, sourceUri, sourceLine
+      // jsniIdent, className, memberName, sourceUri, sourceLine, fragmentId
       String[] parts = symbolData.split(",");
-      if (parts.length == 5) {
+      if (parts.length == 6) {
         String[] ref = parse(
             parts[0].substring(0, parts[0].lastIndexOf(')') + 1));
 
-        String declaringClass;
-        String methodName;
         if (ref != null) {
           declaringClass = ref[0];
           methodName = ref[1];
@@ -133,12 +154,13 @@
           declaringClass = ste.getClassName();
           methodName = ste.getMethodName();
         }
-        
+
         // parts[3] contains the source file URI or "Unknown"
-        String filename = "Unknown".equals(parts[3]) ? null
+        filename = "Unknown".equals(parts[3]) ? null
             : parts[3].substring(parts[3].lastIndexOf('/') + 1);
-        
-        int lineNumber = ste.getLineNumber();
+
+        lineNumber = ste.getLineNumber();
+
         /*
          * When lineNumber is LINE_NUMBER_UNKNOWN, either because
          * compiler.stackMode is not emulated or
@@ -148,30 +170,78 @@
         if (lineNumber == LINE_NUMBER_UNKNOWN) {
           lineNumber = Integer.parseInt(parts[4]);
         }
-        
-        return new StackTraceElement(
-            declaringClass, methodName, filename, lineNumber);
+
+        fragmentId = Integer.parseInt(parts[5]);
       }
     }
+
+    // anonymous function, try to use <fragmentNum>.js:line:col to lookup function
+    if (fragmentId == -1 && steFilename != null) {
+      // fragment identifier encoded in filename
+      Matcher matcher = fragmentIdPattern.matcher(steFilename);
+      if (matcher.matches()) {
+        String fragment = matcher.group(1);
+        try {
+          fragmentId = Integer.parseInt(fragment);
+        } catch (Exception e) {
+        }
+      } else if (steFilename.contains(strongName)) {
+        // else it's <strongName>.cache.js which is the 0th fragment
+        fragmentId = 0;
+      }
+    }
+
+    // try to refine location via sourcemap
+    if (fragmentId != -1) {
+      SourceMapping sourceMapping = loadSourceMap(strongName, fragmentId);
+      if (sourceMapping != null && ste.getLineNumber() > -1) {
+        int column = 1;
+        int jsLineNumber = ste.getLineNumber();
+        if (jsLineNumber > StackTraceCreator.MAX_LINE_NUMBER) {
+          column = jsLineNumber / StackTraceCreator.MAX_LINE_NUMBER;
+          jsLineNumber = jsLineNumber % StackTraceCreator.MAX_LINE_NUMBER;
+        }
+        Mapping.OriginalMapping mappingForLine = sourceMapping
+            .getMappingForLine(jsLineNumber, column);
+        if (mappingForLine != null) {
+
+          if (declaringClass == null || declaringClass.equals(ste.getClassName())) {
+            declaringClass = mappingForLine.getOriginalFile();
+            methodName = mappingForLine.getIdentifier();
+          }
+          filename = mappingForLine.getOriginalFile();
+          lineNumber = mappingForLine.getLineNumber() - 1;
+        }
+      }
+    }
+
+    if (declaringClass != null) {
+      return new StackTraceElement(declaringClass, methodName, filename, lineNumber);
+    }
+
     // If anything goes wrong, just return the unobfuscated element
     return ste;
   }
-  
+
   public void setSymbolMapsDirectory(String symbolMapsDirectory) {
     // permutations are unique, no need to clear the symbolMaps hash map
     this.symbolMapsDirectory = new File(symbolMapsDirectory);
   }
 
+  protected InputStream getSourceMapInputStream(String permutationStrongName, int fragmentNumber)
+      throws IOException {
+    String filename = symbolMapsDirectory.getCanonicalPath()
+        + File.separatorChar + permutationStrongName + "_sourceMap" + fragmentNumber + ".json";
+    return new FileInputStream(filename);
+  }
+
   /**
-   * Retrieves a new {@link InputStream} for the given permutation strong name.
-   * This implementation, which subclasses may override, returns a
-   * {@link InputStream} for the <code>
-   * <i>permutation-strong-name</i>.symbolMap</code> file in the
-   * <code>symbolMaps</code> directory.
+   * Retrieves a new {@link InputStream} for the given permutation strong name. This implementation,
+   * which subclasses may override, returns a {@link InputStream} for the <code>
+   * <i>permutation-strong-name</i>.symbolMap</code> file in the <code>symbolMaps</code> directory.
    *
    * @param permutationStrongName the GWT permutation strong name
    * @return a new {@link InputStream}
-   * @throws IOException
    */
   protected InputStream getSymbolMapInputStream(String permutationStrongName)
       throws IOException {
@@ -179,7 +249,7 @@
         + File.separatorChar + permutationStrongName + ".symbolMap";
     return new FileInputStream(filename);
   }
-  
+
   private Throwable deobfuscateThrowable(Throwable old, String strongName) {
     Throwable t = new Throwable(old.getMessage());
     if (old.getStackTrace() != null) {
@@ -192,7 +262,27 @@
     }
     return t;
   }
-  
+
+  private SourceMapping loadSourceMap(String permutationStrongName, int fragmentId) {
+    SourceMapping toReturn = sourceMaps.get(permutationStrongName + fragmentId);
+    if (toReturn == null) {
+      try {
+        SourceMapConsumerV3 consumer = new SourceMapConsumerV3();
+        String sourceMapString = loadStreamAsString(
+            getSourceMapInputStream(permutationStrongName, fragmentId));
+        JSONObject obj = new JSONObject(sourceMapString);
+        toReturn = SourceMapConsumerFactory.parse(sourceMapString);
+        sourceMaps.put(permutationStrongName + fragmentId, toReturn);
+      } catch (Exception e) {
+      }
+    }
+    return toReturn;
+  }
+
+  private String loadStreamAsString(InputStream stream) {
+    return new Scanner(stream).useDelimiter("\\A").next();
+  }
+
   private SymbolMap loadSymbolMap(
       String strongName) {
     SymbolMap toReturn = symbolMaps.get(strongName);
@@ -212,7 +302,7 @@
           }
           int idx = line.indexOf(',');
           toReturn.put(new String(line.substring(0, idx)),
-                       line.substring(idx + 1));
+              line.substring(idx + 1));
         }
       } finally {
         bin.close();
@@ -227,13 +317,13 @@
   }
 
   /**
-   * Extracts the declaring class and method name from a JSNI ref, or null if
-   * the information cannot be extracted.
+   * Extracts the declaring class and method name from a JSNI ref, or null if the information cannot
+   * be extracted.
    *
-   * @see com.google.gwt.dev.util.JsniRef
    * @param refString symbol map reference string
-   * @return a string array contains the declaring class and method name, or
-   *         null when the regex match fails
+   * @return a string array contains the declaring class and method name, or null when the regex
+   *         match fails
+   * @see com.google.gwt.dev.util.JsniRef
    */
   private String[] parse(String refString) {
     Matcher matcher = JsniRefPattern.matcher(refString);
@@ -242,7 +332,7 @@
     }
     String className = matcher.group(1);
     String memberName = matcher.group(2);
-    String[] toReturn = new String[] {className, memberName};
+    String[] toReturn = new String[]{className, memberName};
     return toReturn;
   }
 }
diff --git a/user/src/com/google/gwt/user/tools/QuerySourceMap.java b/user/src/com/google/gwt/user/tools/QuerySourceMap.java
new file mode 100644
index 0000000..8d7b3a8
--- /dev/null
+++ b/user/src/com/google/gwt/user/tools/QuerySourceMap.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2009 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.gwt.user.tools;
+
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerFactory;
+import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapping;
+import com.google.gwt.dev.util.Util;
+
+import java.io.File;
+
+/**
+ * Command-line utility for querying source maps.
+ */
+public class QuerySourceMap {
+
+  public static void main(String[] args) throws Exception {
+    String filename = args[0];
+    int line = Integer.valueOf(args[1]);
+    int col = Integer.valueOf(args[2]);
+
+    SourceMapping consumer = SourceMapConsumerFactory.parse(Util.readFileAsString(new File(filename)));
+    System.out.println(consumer.getMappingForLine(line, col));
+  }
+}
diff --git a/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java b/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
index e57776a..cd7e41d 100644
--- a/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
+++ b/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
@@ -185,12 +185,12 @@
   public void testChromeExtractName() {
     CollectorChrome c = new CollectorChrome();
 
-    assertEquals("anonymous", c.extractName(" at file.js:1:2"));
-    assertEquals("functionName",
+    assertEquals("anonymous@@file.js:1:2", c.extractName(" at file.js:1:2"));
+    assertEquals("functionName@@file.js:1:2",
         c.extractName(" at functionName (file.js:1:2)"));
-    assertEquals("functionName",
+    assertEquals("functionName@@file.js:1:2",
         c.extractName(" at Type.functionName (file.js:1:2)"));
-    assertEquals("functionName",
+    assertEquals("functionName@@file.js:1:2",
         c.extractName(" at Type.functionName [as methodName] (file.js:1:2)"));
   }
 }
diff --git a/user/test/com/google/gwt/user/LoggingRPCSuite.gwt.xml b/user/test/com/google/gwt/user/LoggingRPCSuite.gwt.xml
index 6f589f6..d8f2109 100644
--- a/user/test/com/google/gwt/user/LoggingRPCSuite.gwt.xml
+++ b/user/test/com/google/gwt/user/LoggingRPCSuite.gwt.xml
@@ -21,5 +21,32 @@
 
   <servlet path='/loggingrpc'
     class='com.google.gwt.user.server.rpc.LoggingRPCTestServiceImpl' />
+  <servlet path="/remote_logging" class="com.google.gwt.logging.server.RemoteLoggingServiceImpl"/>
+
+  <set-property name="gwt.logging.logLevel" value="WARNING"/>
+  <set-property name="gwt.logging.enabled" value="TRUE"/>
+  <set-property name="gwt.logging.consoleHandler" value="ENABLED"/>
+  <set-property name="gwt.logging.simpleRemoteHandler" value="ENABLED" />
+
+
+  <set-property name="compiler.useSourceMaps" value="true"/>
+
+
+  <!-- Turn off emulation to test non-sourcemap mode -->
+  <set-property name="compiler.stackMode" value="native"/> 
+  <!-- when Chrome is enabled, turn off stack trace emulation -->
+  <set-property name="compiler.stackMode" value="strip"> 
+    <when-property-is name="user.agent" value="safari"/>
+  </set-property>
+
+  <!-- when stack trace stripping is enabled, we need to replace the Null collector 
+       with one that does something for Chrome -->
+  <replace-with class="com.google.gwt.core.client.impl.StackTraceCreator.CollectorChrome">
+    <when-type-is class="com.google.gwt.core.client.impl.StackTraceCreator.Collector" />
+    <!-- For now, only Chrome provides Error.stack support, so we hijack the
+         entire WebKit permutation -->
+    <when-property-is name="user.agent" value="safari" />
+    <when-property-is name="compiler.useSourceMaps" value="true" />
+  </replace-with>
 
 </module>
diff --git a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTest.java b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTest.java
index 365ce4a..8991ffa 100644
--- a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTest.java
@@ -18,21 +18,34 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.core.client.impl.SourceMapProperty;
 
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 
 /**
  * Test cases for Generic Collections in GWT RPC.
- * 
  */
 public class LoggingRPCTest extends GWTTestCase {
 
+  /**
+   * WARNING! WARNING! If you edit this method or insert any lines of code above this method,you
+   * must re-edit the line number constants below;
+   */
+  private static final int METHOD_START_LINE = 39;
+
+  private static final int METHOD_EXCEPTION_LINE = 41;
+
+  private void throwException(String arg) {
+    // prevent inlining by double referencing arg
+    throw new RuntimeException(arg.charAt(0) + arg.substring(1));
+  }
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.user.LoggingRPCSuite";
   }
-  
+
   private static final LogRecord expectedLogRecord = createLogRecord();
 
   public static boolean isValid(LogRecord value) {
@@ -64,7 +77,7 @@
       }
 
       // Do not compare trace as it is not stable across RPC.
-      
+
       expectedCause = expectedCause.getCause();
       valueCause = valueCause.getCause();
     }
@@ -90,6 +103,18 @@
     return result;
   }
 
+  private static LogRecord createLogRealRecord(Throwable thrown) {
+    LogRecord result = new LogRecord(Level.INFO, "Test Log Record");
+
+    // Only set serialized fields.
+    result.setLoggerName("Test Logger Name");
+    result.setMillis(1234567);
+
+    result.setThrown(thrown);
+
+    return result;
+  }
+
   private LoggingRPCTestServiceAsync loggingRPCTestService;
 
   public void testLogRecord() {
@@ -109,10 +134,65 @@
     });
   }
 
+  public void testStackMapDeobfuscation() {
+    final LoggingRPCTestServiceAsync service = getServiceAsync();
+    delayTestFinish(500000);
+    GWT.runAsync(LoggingRPCTest.class, new com.google.gwt.core.client.RunAsyncCallback() {
+
+      @Override
+      public void onFailure(Throwable reason) {
+      }
+
+      @Override
+      public void onSuccess() {
+        try {
+          throwException("Runtime Exception");
+        } catch (Exception e) {
+          service.deobfuscateLogRecord(createLogRealRecord(e), new AsyncCallback<LogRecord>() {
+
+            public void onFailure(Throwable caught) {
+              assertTrue(false);
+              TestSetValidator.rethrowException(caught);
+            }
+
+            public void onSuccess(LogRecord record) {
+              Throwable thrown = record.getThrown();
+              boolean found = false;
+              for (StackTraceElement e : thrown.getStackTrace()) {
+                if (e.getFileName().contains("LoggingRPCTest")) {
+                  // if DevMode or SourceMaps enabled and Chrome is the browser, check for exact line
+                  if (SourceMapProperty.isDetailedDeobfuscatedStackTraceSupported()) {
+                    assertEquals(METHOD_EXCEPTION_LINE, e.getLineNumber());
+                  } else {
+                    // else fallback to line number of method itself
+                    assertEquals(METHOD_START_LINE, e.getLineNumber());
+                  }
+                  String methodName = "throwException";
+                  assertTrue("Method name mismatch, expected = " + methodName
+                      + " vs actual = " + e.getMethodName(),
+                      e.getMethodName().contains(methodName));
+                  String className = "com.google.gwt.user.client.rpc.LoggingRPCTest";
+                  assertTrue("Class name mismatch, expected = " + className
+                      + " actual = " + e.getClassName(), className.contains(e.getClassName()));
+                  found = true;
+                  break;
+                }
+              }
+              assertTrue(found);
+              finishTest();
+            }
+          });
+        }
+      }
+    });
+  }
+
   private LoggingRPCTestServiceAsync getServiceAsync() {
     if (loggingRPCTestService == null) {
       loggingRPCTestService = (LoggingRPCTestServiceAsync) GWT.create(LoggingRPCTestService.class);
     }
     return loggingRPCTestService;
   }
+
+
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestService.java b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestService.java
index f597cd2..1cc4842 100644
--- a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestService.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2011 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
@@ -34,6 +34,7 @@
       super(msg);
     }
   }
+  LogRecord deobfuscateLogRecord(LogRecord value) throws LoggingRPCTestServiceException;
 
   LogRecord echoLogRecord(LogRecord value) throws LoggingRPCTestServiceException;
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestServiceAsync.java
index 6e680a0..0460667 100644
--- a/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/LoggingRPCTestServiceAsync.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2011 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
@@ -23,6 +23,8 @@
  * Async interface for serialization of GWT core.java emulations.
  */
 public interface LoggingRPCTestServiceAsync {
+  void deobfuscateLogRecord(LogRecord value, AsyncCallback<LogRecord> callback);
 
   void echoLogRecord(LogRecord value, AsyncCallback<LogRecord> callback);
+
 }
diff --git a/user/test/com/google/gwt/user/server/rpc/LoggingRPCTestServiceImpl.java b/user/test/com/google/gwt/user/server/rpc/LoggingRPCTestServiceImpl.java
index cb61242..691139a 100644
--- a/user/test/com/google/gwt/user/server/rpc/LoggingRPCTestServiceImpl.java
+++ b/user/test/com/google/gwt/user/server/rpc/LoggingRPCTestServiceImpl.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2011 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
@@ -16,18 +16,80 @@
 
 package com.google.gwt.user.server.rpc;
 
+import static com.google.gwt.user.client.rpc.RpcRequestBuilder.MODULE_BASE_HEADER;
+
+import com.google.gwt.junit.linker.JUnitSymbolMapsLinker;
+import com.google.gwt.logging.server.StackTraceDeobfuscator;
 import com.google.gwt.user.client.rpc.LoggingRPCTest;
 import com.google.gwt.user.client.rpc.LoggingRPCTestService;
+import com.google.gwt.user.client.rpc.RpcRequestBuilder;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
 
 /**
- * Remote service implementation for serialization of GWT core.java.util.logging
- * emulations.
+ * Remote service implementation for serialization of GWT core.java.util.logging emulations.
  */
 public class LoggingRPCTestServiceImpl extends HybridServiceServlet implements
     LoggingRPCTestService {
 
+  @Override
+  public LogRecord deobfuscateLogRecord(LogRecord value)
+      throws LoggingRPCTestServiceException {
+    // don't deobfuscate DevMode, there's no symbol map
+    if ("HostedMode".equals(getPermutationStrongName())) {
+      return value;
+    }
+
+    StackTraceDeobfuscator deobf = new StackTraceDeobfuscator(getSymbolMapsDir()) {
+      @Override
+      protected InputStream getSourceMapInputStream(String permutationStrongName,
+          int fragmentNumber)
+          throws IOException {
+        if (symbolMapsDirectory.exists()) {
+          return super.getSourceMapInputStream(permutationStrongName,
+              fragmentNumber);
+        } else {
+          return getServletContext().getResourceAsStream(
+              getModule() + File.separatorChar + JUnitSymbolMapsLinker.SYMBOL_MAP_DIR
+                  + permutationStrongName
+                  + "_sourceMap" + fragmentNumber + ".json");
+        }
+      }
+
+      @Override
+      protected InputStream getSymbolMapInputStream(String permutationStrongName)
+          throws IOException {
+        if (symbolMapsDirectory.exists()) {
+          return super.getSymbolMapInputStream(permutationStrongName);
+        } else {
+          String name =
+              getModule() + File.separatorChar + JUnitSymbolMapsLinker.SYMBOL_MAP_DIR
+                  + permutationStrongName
+                  + ".symbolMap";
+          return getServletContext().getResourceAsStream(name);
+        }
+      }
+    };
+
+    HttpServletRequest threadLocalRequest = getThreadLocalRequest();
+    String strongName = null;
+    if (threadLocalRequest != null) {
+      // can be null during tests
+      strongName = threadLocalRequest.getHeader(RpcRequestBuilder.STRONG_NAME_HEADER);
+    }
+    LogRecord newRecord = deobf.deobfuscateLogRecord(value, strongName);
+    Logger.getLogger(value.getLoggerName()).log(newRecord);
+    return newRecord;
+  }
+
   public LogRecord echoLogRecord(LogRecord value) throws LoggingRPCTestServiceException {
     /*
      * Don't check the stack trace on the server side, because the expected
@@ -41,4 +103,34 @@
 
     return value;
   }
+
+  private String getModule() {
+    try {
+      String header = getThreadLocalRequest().getHeader(MODULE_BASE_HEADER);
+      if (header == null) {
+        return null;
+      }
+      String path = new URL(header).getPath();
+      String contextPath = getThreadLocalRequest().getContextPath();
+      if (!path.startsWith(contextPath)) {
+        return null;
+      }
+      path = path.substring(contextPath.length());
+      if (path.endsWith("/")) {
+        path = path.substring(0, path.length() - 1);
+      }
+      return path;
+    } catch (MalformedURLException e) {
+      return null;
+    }
+  }
+
+  private String getSymbolMapsDir() {
+    String path = getModule();
+    if (path == null) {
+      return null;
+    }
+    return "war" + File.separatorChar + getModule() + File.separatorChar
+        + JUnitSymbolMapsLinker.SYMBOL_MAP_DIR;
+  }
 }