Super Dev Mode: faster sourcemap loading

Instead of parsing the sourcemap as json and writing it out again,
replace a template variable while streaming the file.

To support this, I gave the GWT compiler the ability to set the
base URL in the sourcemap. (There's an option for this but it's
not exposed as a flag yet.)

Also, cleaned up the code server's internals and sourcemap
generation. (We no longer need to subclass SourceMapRecorder
to populate the "names" field in the sourcemap.)

Change-Id: I51ff3f0970aadb832b8f430175b103b104e8fd62
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
index 07a9c06..9bea351 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
@@ -35,24 +35,20 @@
 class CompilerOptionsImpl extends UnmodifiableCompilerOptions {
   private final CompileDir compileDir;
   private final boolean failOnError;
-  private final List<String> libraryPaths;
   private final TreeLogger.Type logLevel;
   private final List<String> moduleNames;
   private final SourceLevel sourceLevel;
   private final boolean strictPublicResources;
   private final boolean strictSourceResources;
 
-  CompilerOptionsImpl(CompileDir compileDir, List<String> moduleNames, SourceLevel sourceLevel,
-      boolean failOnError, boolean strictSourceResources, boolean strictPublicResources,
-      TreeLogger.Type logLevel) {
+  CompilerOptionsImpl(CompileDir compileDir, String moduleName, Options options) {
     this.compileDir = compileDir;
-    this.libraryPaths = ImmutableList.<String> of();
-    this.moduleNames = Lists.newArrayList(moduleNames);
-    this.sourceLevel = sourceLevel;
-    this.failOnError = failOnError;
-    this.strictSourceResources = strictSourceResources;
-    this.strictPublicResources = strictPublicResources;
-    this.logLevel = logLevel;
+    this.moduleNames = Lists.newArrayList(moduleName);
+    this.sourceLevel = options.getSourceLevel();
+    this.failOnError = options.isFailOnError();
+    this.strictSourceResources = options.enforceStrictResources();
+    this.strictPublicResources = options.enforceStrictResources();
+    this.logLevel = options.getLogLevel();
   }
 
   @Override
@@ -97,7 +93,7 @@
 
   @Override
   public List<String> getLibraryPaths() {
-    return libraryPaths;
+    return ImmutableList.of();
   }
 
   /**
@@ -251,7 +247,7 @@
 
   @Override
   public boolean shouldAddRuntimeChecks() {
-    // TODO set to true in a separate patch
+    // Not needed since no optimizations are on.
     return false;
   }
 
@@ -291,6 +287,11 @@
   }
 
   @Override
+  public String getSourceMapFilePrefix() {
+    return SourceHandler.SOURCEROOT_TEMPLATE_VARIABLE;
+  }
+
+  @Override
   public boolean warnOverlappingSource() {
     return false;
   }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
index bf67484..6ec0a5d 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
@@ -18,8 +18,11 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.dev.json.JsonObject;
+import com.google.gwt.thirdparty.guava.common.base.Charsets;
+import com.google.gwt.thirdparty.guava.common.io.Files;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -29,6 +32,8 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Reader;
 import java.io.Writer;
 import java.net.URL;
 import java.util.Date;
@@ -127,6 +132,33 @@
   }
 
   /**
+   * Sends a text file, substituting one variable. (Doesn't preserve line endings.)
+   * @param templateVariable the string to replace
+   * @param replacement the replacement
+   */
+  static void sendTemplateFile(String mimeType, File file, String templateVariable,
+      String replacement, HttpServletResponse response) throws IOException {
+
+    BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
+    try {
+      response.setStatus(HttpServletResponse.SC_OK);
+      response.setContentType(mimeType);
+      PrintWriter out = response.getWriter();
+      while (true) {
+        String line = reader.readLine();
+        if (line == null) {
+          break;
+        }
+        line = line.replace(templateVariable, replacement);
+        out.println(line);
+      }
+    } finally {
+      reader.close();
+    }
+  }
+
+
+  /**
    * Sends a page. Closes pageBytes when done.
    */
   static void sendStream(String mimeType, InputStream pageBytes, HttpServletResponse response)
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 3db96af..48c0ff3 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -38,7 +38,6 @@
 import com.google.gwt.dev.util.log.CompositeTreeLogger;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.thirdparty.guava.common.base.Joiner;
-import com.google.gwt.thirdparty.guava.common.collect.Lists;
 
 import java.io.File;
 import java.io.IOException;
@@ -205,22 +204,19 @@
 
   private boolean compileMonolithic(TreeLogger compileLogger, Map<String, String> bindingProperties,
       CompileDir compileDir) throws UnableToCompleteException {
-    CompilerOptions compilerOptions = new CompilerOptionsImpl(compileDir,
-        options.getModuleNames(), options.getSourceLevel(), options.isFailOnError(),
-        options.enforceStrictResources(), options.enforceStrictResources(),
-        options.getLogLevel());
-    compilerContext = compilerContextBuilder.options(compilerOptions).build();
+
+    CompilerOptions loadOptions = new CompilerOptionsImpl(compileDir, originalModuleName, options);
+    compilerContext = compilerContextBuilder.options(loadOptions).build();
     ModuleDef module = loadModule(compileLogger, bindingProperties);
 
     // Propagates module rename.
     String newModuleName = module.getName();
     moduleName.set(newModuleName);
-    compilerOptions = new CompilerOptionsImpl(compileDir, Lists.newArrayList(newModuleName),
-        options.getSourceLevel(), options.isFailOnError(), options.enforceStrictResources(),
-        options.enforceStrictResources(), options.getLogLevel());
-    compilerContext = compilerContextBuilder.options(compilerOptions).build();
 
-    boolean success = new Compiler(compilerOptions).run(compileLogger, module);
+    CompilerOptions runOptions = new CompilerOptionsImpl(compileDir, newModuleName, options);
+    compilerContext = compilerContextBuilder.options(runOptions).build();
+
+    boolean success = new Compiler(runOptions).run(compileLogger, module);
     if (success) {
       publishedCompileDir = compileDir;
     }
@@ -345,13 +341,12 @@
 
     logger = logger.branch(TreeLogger.Type.INFO, "binding: " + propName + "=" + newValue);
 
-    BindingProperty prop = module.getProperties().findBindingProp(propName);
-    if (prop == null) {
+    BindingProperty binding = module.getProperties().findBindingProp(propName);
+    if (binding == null) {
       logger.log(TreeLogger.Type.WARN, "undefined property: '" + propName + "'");
       return;
     }
 
-    BindingProperty binding = (BindingProperty) prop;
     if (!binding.isAllowedValue(newValue)) {
 
       String[] allowedValues = binding.getAllowedValues(binding.getRootCondition());
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
index e3bc45b..afe5795 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
@@ -47,6 +47,8 @@
    */
   static final String SOURCEMAP_PATH = "/sourcemaps/";
 
+  static final String SOURCEROOT_TEMPLATE_VARIABLE = "$sourceroot_goes_here$";
+
   private Modules modules;
 
   private final TreeLogger logger;
@@ -109,15 +111,29 @@
   private void sendSourceMap(String moduleName, HttpServletRequest request,
       HttpServletResponse response) throws IOException {
 
-    SourceMap map = loadSourceMap(moduleName);
+    long startTime = System.currentTimeMillis();
 
-    // hack: rewrite the source map so that each filename is a URL
-    String serverPrefix = String.format("http://%s:%d/sourcemaps/%s/", request.getServerName(),
+    ModuleState moduleState = modules.get(moduleName);
+    File sourceMap = moduleState.findSourceMap();
+
+    // Stream the file, substituting the sourceroot variable with the filename.
+    // (This is more efficient than parsing the file as JSON.)
+
+    // We need to do this at runtime because we don't know what the hostname will be
+    // until we get a request. (For example, some people run the Code Server behind
+    // a reverse proxy to support https.)
+
+    String sourceRoot = String.format("http://%s:%d/sourcemaps/%s/", request.getServerName(),
         request.getServerPort(), moduleName);
-    map.addPrefixToEachSourceFile(serverPrefix);
 
-    PageUtil.sendString("application/json", map.serialize(), response);
-    logger.log(TreeLogger.WARN, "sent source map for module: " + moduleName);
+    PageUtil.sendTemplateFile("application/json", sourceMap,
+        "\"" + SOURCEROOT_TEMPLATE_VARIABLE + "\"",
+        "\"" + sourceRoot + "\"", response);
+
+    long elapsedTime = System.currentTimeMillis() - startTime;
+
+    logger.log(TreeLogger.WARN, "sent source map for module '" + moduleName +
+        "' in " + elapsedTime + " ms");
   }
 
   private void sendDirectoryListPage(String moduleName, HttpServletResponse response)
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceMap.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceMap.java
index 0ca184c..f48610a 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceMap.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceMap.java
@@ -24,7 +24,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
-import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -60,19 +59,6 @@
   }
 
   /**
-   * Adds the given prefix to each source filename in the source map.
-   */
-  void addPrefixToEachSourceFile(String serverPrefix) {
-    JsonArray sources = (JsonArray) json.get("sources");
-    JsonArray newSources = new JsonArray();
-    for (int i = 0; i < sources.getLength(); i++) {
-      String filename = sources.get(i).asString().getString();
-      newSources.add(serverPrefix + filename);
-    }
-    json.put("sources", newSources);
-  }
-
-  /**
    * Returns a sorted list of all the directories containing at least one filename
    * in the source map.
    */
@@ -115,14 +101,4 @@
 
     return result;
   }
-
-  String serialize() {
-    StringWriter buffer = new StringWriter();
-    try {
-      json.write(buffer);
-    } catch (IOException e) {
-      throw new RuntimeException("can't convert sourcemap to json");
-    }
-    return buffer.toString();
-  }
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
index 0990d69..516126e 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
@@ -235,6 +235,11 @@
   }
 
   @Override
+  public void setSourceMapFilePrefix(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public final void setSoycEnabled(boolean enabled) {
     throw new UnsupportedOperationException();
   }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/dev_mode_on.js b/dev/codeserver/java/com/google/gwt/dev/codeserver/dev_mode_on.js
index 7481833..6948cd0 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/dev_mode_on.js
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/dev_mode_on.js
@@ -94,7 +94,7 @@
     overlay.style.left = 0;
     overlay.style.bottom = 0;
     overlay.style.right = 0;
-    overlay.style.background = 'white';
+    overlay.style.background = 'black'; // darken background
     overlay.style.opacity = '0.5';
     return overlay;
   }
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
index 564c88f..0c744ae 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorder.java
@@ -17,9 +17,11 @@
 
 import com.google.gwt.core.ext.linker.SyntheticArtifact;
 import com.google.gwt.core.linker.SymbolMapsLinker;
+import com.google.gwt.dev.jjs.Correlation;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.JsSourceMap;
 import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.jjs.SourceInfoCorrelation;
 import com.google.gwt.dev.jjs.SourceOrigin;
 import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorV3;
 import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapParseException;
@@ -39,63 +41,81 @@
  */
 public class SourceMapRecorder {
 
-  public static List<SyntheticArtifact> makeSourceMapArtifacts(int permutationId,
-      List<JsSourceMap> sourceInfoMaps) {
+  /**
+   * Generates a sourcemap for each fragment in the list.
+   *
+   * @param sourceFilePrefix the prefix that a debugger should add to the beginning of each
+   * filename in a sourcemap to determine the file's full URL.
+   * If null, filenames are relative to the sourcemap's URL.
+   */
+  public static List<SyntheticArtifact> exec(int permutationId,
+      List<JsSourceMap> fragmentMaps, String sourceFilePrefix) {
     try {
-      return (new SourceMapRecorder(permutationId)).recordSourceMap(sourceInfoMaps);
+      return new SourceMapRecorder(permutationId, fragmentMaps, sourceFilePrefix).createArtifacts();
     } catch (Exception e) {
       throw new InternalCompilerException(e.toString(), e);
     }
   }
 
-  protected final int permutationId;
-
-  protected SourceMapRecorder(int permutationId) {
-    this.permutationId = permutationId;
+  /**
+   * Generates a sourcemap for each fragment in the list, with JavaScript-to-Java
+   * name mappings included.
+   */
+  public static List<SyntheticArtifact> execWithJavaNames(int permutationId,
+      List<JsSourceMap> fragmentMaps, String sourceFilePrefix) {
+    try {
+      SourceMapRecorder recorder = new SourceMapRecorder(permutationId, fragmentMaps,
+          sourceFilePrefix);
+      recorder.wantJavaNames = true;
+      return recorder.createArtifacts();
+    } catch (Exception e) {
+      throw new InternalCompilerException(e.toString(), e);
+    }
   }
 
-  protected List<SyntheticArtifact> recordSourceMap(List<JsSourceMap> sourceInfoMaps)
+  private final int permutationId;
+  private final List<JsSourceMap> fragmentMaps;
+  private final String sourceRoot;
+  private boolean wantJavaNames;
+
+  private SourceMapRecorder(int permutationId, List<JsSourceMap> fragmentMaps, String sourceRoot) {
+    this.permutationId = permutationId;
+    this.fragmentMaps = fragmentMaps;
+    this.sourceRoot = sourceRoot;
+  }
+
+  private List<SyntheticArtifact> createArtifacts()
       throws IOException, JSONException, SourceMapParseException {
     List<SyntheticArtifact> toReturn = Lists.newArrayList();
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
     int fragment = 0;
-    if (!sourceInfoMaps.isEmpty()) {
-      for (JsSourceMap sourceMap : sourceInfoMaps) {
-        generator.reset();
-        addMappings(new SourceMappingWriter(generator), sourceMap);
-        updateSourceMap(generator, fragment);
+    for (JsSourceMap sourceMap : fragmentMaps) {
+      generator.reset();
 
-        baos.reset();
-        OutputStreamWriter out = new OutputStreamWriter(baos);
-        generator.appendTo(out, "sourceMap" + fragment);
-        out.flush();
-        toReturn.add(new SymbolMapsLinker.SourceMapArtifact(permutationId, fragment,
-            baos.toByteArray()));
-        fragment++;
+      if (sourceRoot != null) {
+        generator.setSourceRoot(sourceRoot);
       }
+      addExtensions(generator, fragment);
+      addMappings(new SourceMappingWriter(generator), sourceMap);
+
+      baos.reset();
+      OutputStreamWriter out = new OutputStreamWriter(baos);
+      generator.appendTo(out, "sourceMap" + fragment);
+      out.flush();
+      toReturn.add(new SymbolMapsLinker.SourceMapArtifact(permutationId, fragment,
+          baos.toByteArray(), sourceRoot));
+      fragment++;
     }
     return toReturn;
   }
 
-  /**
-   * A hook allowing a subclass to add more info to the sourcemap for a given fragment.
-   */
-  protected void updateSourceMap(SourceMapGeneratorV3 generator, int fragment)
-      throws SourceMapParseException { }
-
-  /**
-   * A hook allowing a subclass to populate the "names" field in the sourcemap.
-   *
-   * <p>The name is currently always a Java identifier, but in theory may be any Java expression.
-   * For example, a compiler-introduced temporary variable could be annotated with the expression
-   * that produced it.
-   *
-   * <p>The name should only be set if the JavaScript range covers one JavaScript identifier.
-   * (Otherwise return null.)
-   */
-  protected String getJavaName(SourceInfo sourceInfo) {
-    return null;
+  private void addExtensions(SourceMapGeneratorV3 generator, int fragment)
+      throws SourceMapParseException {
+    // We don't convert to a string here so that the values will be added
+    // to the JSON as a number instead of a string.
+    generator.addExtension("x_gwt_permutation", permutationId);
+    generator.addExtension("x_gwt_fragment", fragment);
   }
 
   /**
@@ -126,4 +146,35 @@
     }
     output.flush();
   }
+
+  /**
+   * Returns the name to be added to the "names" field in the sourcemap.
+   *
+   * <p>The name is currently always a Java identifier, but in theory may be any Java expression.
+   * For example, a compiler-introduced temporary variable could be annotated with the expression
+   * that produced it.
+   *
+   * <p>The name should only be set if the JavaScript range covers one JavaScript identifier.
+   * (Otherwise return null.)
+   */
+  private String getJavaName(SourceInfo sourceInfo) {
+
+    if (!wantJavaNames) {
+      return null;
+    }
+
+    if (!(sourceInfo instanceof SourceInfoCorrelation)) {
+      return null;
+    }
+
+    Correlation correlation = ((SourceInfoCorrelation) sourceInfo).getPrimaryCorrelation();
+    if (correlation == null) {
+      return null;
+    }
+
+    // Conserve space by not recording the package name. The sourcemap already contains the full
+    // path of the Java file (in the "sources" field), which is usually enough to identify
+    // the package. (The name may be a synthetic method name.)
+    return correlation.getIdent();
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorderExt.java b/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorderExt.java
deleted file mode 100644
index 0a4934c..0000000
--- a/dev/core/src/com/google/gwt/core/ext/soyc/SourceMapRecorderExt.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2013 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.core.ext.linker.SyntheticArtifact;
-import com.google.gwt.dev.jjs.Correlation;
-import com.google.gwt.dev.jjs.InternalCompilerException;
-import com.google.gwt.dev.jjs.JsSourceMap;
-import com.google.gwt.dev.jjs.SourceInfo;
-import com.google.gwt.dev.jjs.SourceInfoCorrelation;
-import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorV3;
-import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapParseException;
-
-import java.util.List;
-
-/**
- * Creates Closure Compatible SourceMaps with named ranges.
- */
-public class SourceMapRecorderExt extends SourceMapRecorder {
-
-  public static final String PERMUTATION_EXT = "x_gwt_permutation";
-
-  public static List<SyntheticArtifact> makeSourceMapArtifacts(int permutationId,
-      List<JsSourceMap> sourceInfoMaps) {
-    try {
-      return (new SourceMapRecorderExt(permutationId)).recordSourceMap(sourceInfoMaps);
-    } catch (Exception e) {
-      throw new InternalCompilerException(e.toString(), e);
-    }
-  }
-
-  protected SourceMapRecorderExt(int permutationId) {
-    super(permutationId);
-  }
-
-  @Override
-  protected void updateSourceMap(SourceMapGeneratorV3 generator, int fragment)
-      throws SourceMapParseException {
-    generator.addExtension(PERMUTATION_EXT, new Integer(permutationId));
-    generator.addExtension("x_gwt_fragment", new Integer(fragment));
-  }
-
-  @Override
-  protected String getJavaName(SourceInfo sourceInfo) {
-    // We can discard Unknown or not-so-valid (eg. com.google.gwt.dev.js.ast.JsProgram)
-    // sourceInfo
-    String rangeName = null;
-    if (sourceInfo instanceof SourceInfoCorrelation) {
-      Correlation correlation = ((SourceInfoCorrelation) sourceInfo).getPrimaryCorrelation();
-      if (correlation != null) {
-        // We can reduce name sizes by removing the left part corresponding to the
-        // package name, eg. com.google.gwt.client. Because this is already in the file name.
-        // This name includes static/synth method names
-        rangeName = correlation.getIdent();
-      }
-    }
-    return rangeName;
-  }
-}
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/coderef/EntityRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/coderef/EntityRecorder.java
index d54f20e..69d6daa 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/coderef/EntityRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/coderef/EntityRecorder.java
@@ -17,7 +17,7 @@
 
 import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
 import com.google.gwt.core.ext.linker.SyntheticArtifact;
-import com.google.gwt.core.ext.soyc.SourceMapRecorderExt;
+import com.google.gwt.core.ext.soyc.SourceMapRecorder;
 import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor.Fragment;
 import com.google.gwt.core.linker.SoycReportLinker;
 import com.google.gwt.dev.jjs.InternalCompilerException;
@@ -63,21 +63,22 @@
   public static final String INITIAL_SEQUENCE = "initialSequence";
 
   public static List<SyntheticArtifact> makeSoycArtifacts(int permutationId,
-      List<JsSourceMap> sourceInfoMaps, JavaToJavaScriptMap jjsmap,
+      List<JsSourceMap> sourceInfoMaps, String sourceMapFilePrefix, JavaToJavaScriptMap jjsmap,
       SizeBreakdown[] sizeBreakdowns, DependencyGraphRecorder codeGraph, JProgram jprogram) {
 
-    EntityRecorder recorder = new EntityRecorder(sizeBreakdowns, permutationId);
+    List<SyntheticArtifact> artifacts = Lists.newArrayList();
     try {
+      EntityRecorder recorder = new EntityRecorder(sizeBreakdowns, permutationId);
       recorder.recordCodeReferences(codeGraph, sizeBreakdowns, jjsmap);
       recorder.recordFragments(jprogram);
-      // record source map with named ranges
-      recorder.toReturn.addAll(SourceMapRecorderExt.makeSourceMapArtifacts(
-          permutationId, sourceInfoMaps));
+      artifacts.addAll(recorder.toReturn);
+      artifacts.addAll(SourceMapRecorder.execWithJavaNames(permutationId, sourceInfoMaps,
+          sourceMapFilePrefix));
     } catch (Exception e) {
       throw new InternalCompilerException(e.toString(), e);
     }
 
-    return recorder.toReturn;
+    return artifacts;
   }
 
   private final List<SyntheticArtifact> toReturn = Lists.newArrayList();
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 6ba1bd1..8b50d17 100644
--- a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
@@ -174,11 +174,14 @@
     private int fragment;
     private byte[] js;
 
-    public SourceMapArtifact(int permutationId, int fragment, byte[] js) {
+    private final String sourceRoot;
+
+    public SourceMapArtifact(int permutationId, int fragment, byte[] js, String sourceRoot) {
       super(SymbolMapsLinker.class, permutationId + '/' + sourceMapFilenameForFragment(fragment), js);
       this.permutationId = permutationId;
       this.fragment = fragment;
       this.js = js;
+      this.sourceRoot = sourceRoot;
     }
 
     public int getFragment() {
@@ -189,6 +192,14 @@
       return permutationId;
     }
 
+    /**
+     * The base URL for Java filenames in the sourcemap.
+     * (We need to reapply this after edits.)
+     */
+    public String getSourceRoot() {
+      return sourceRoot;
+    }
+
     public static String sourceMapFilenameForFragment(int fragment) {
       // If this changes, update isSourceMapFile.
       return "sourceMap" + fragment + ".json";
@@ -298,6 +309,12 @@
           emArt = emitSourceMapString(logger, sourceMapString, partialPath);
         } else {
           SourceMapGeneratorV3 sourceMapGenerator = new SourceMapGeneratorV3();
+
+          if (se.getSourceRoot() != null) {
+            // Reapply source root since mergeMapSection() will not copy it.
+            sourceMapGenerator.setSourceRoot(se.getSourceRoot());
+          }
+
           try {
             int totalPrefixLines = 0;
             for (ScriptFragmentEditsArtifact.EditOperation op : editArtifact.editOperations) {
diff --git a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
index fc58bfd..8475edf 100644
--- a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
+++ b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
@@ -22,6 +22,7 @@
 import com.google.gwt.dev.util.arg.OptionMaxPermsPerPrecompile;
 import com.google.gwt.dev.util.arg.OptionMissingDepsFile;
 import com.google.gwt.dev.util.arg.OptionSaveSource;
+import com.google.gwt.dev.util.arg.OptionSourceMapFilePrefix;
 import com.google.gwt.dev.util.arg.OptionValidateOnly;
 import com.google.gwt.dev.util.arg.OptionWarnMissingDeps;
 import com.google.gwt.dev.util.arg.OptionWarnOverlappingSource;
@@ -30,7 +31,7 @@
  * The set of options for the Precompiler.
  */
 public interface PrecompileTaskOptions extends JJSOptions, CompileTaskOptions, OptionGenDir,
-    OptionSaveSource, OptionValidateOnly, OptionDisableUpdateCheck, OptionEnableGeneratingOnShards,
-    OptionMaxPermsPerPrecompile, OptionMissingDepsFile, OptionWarnOverlappingSource,
-    OptionWarnMissingDeps, PrecompilationResult {
+    OptionSaveSource, OptionSourceMapFilePrefix, OptionValidateOnly, OptionDisableUpdateCheck,
+    OptionEnableGeneratingOnShards, OptionMaxPermsPerPrecompile, OptionMissingDepsFile,
+    OptionWarnOverlappingSource, OptionWarnMissingDeps, PrecompilationResult {
 }
diff --git a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
index e39b11d..ea13197 100644
--- a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
+++ b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
@@ -36,6 +36,7 @@
   private int maxPermsPerPrecompile;
   private File missingDepsFile;
   private boolean saveSource;
+  private String sourceMapFilePrefix;
   private boolean validateOnly;
   private boolean warnOverlappingSource;
   private boolean warnMissingDeps;
@@ -67,6 +68,7 @@
     setDisableUpdateCheck(other.isUpdateCheckDisabled());
     setGenDir(other.getGenDir());
     setSaveSource(other.shouldSaveSource());
+    setSourceMapFilePrefix(other.getSourceMapFilePrefix());
     setMaxPermsPerPrecompile(other.getMaxPermsPerPrecompile());
     setWarnOverlappingSource(other.warnOverlappingSource());
     setWarnMissingDeps(other.warnMissingDeps());
@@ -132,6 +134,11 @@
   }
 
   @Override
+  public String getSourceMapFilePrefix() {
+    return sourceMapFilePrefix;
+  }
+
+  @Override
   @Deprecated
   public boolean isAggressivelyOptimize() {
     return jjsOptions.isAggressivelyOptimize();
@@ -363,6 +370,11 @@
   }
 
   @Override
+  public void setSourceMapFilePrefix(String path) {
+    sourceMapFilePrefix = path;
+  }
+
+  @Override
   public void setSoycEnabled(boolean enabled) {
     jjsOptions.setSoycEnabled(enabled);
   }
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 cdd6e94..c4a96df 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -392,7 +392,8 @@
         } else {
           // Is a super set of SourceMapRecorder.makeSourceMapArtifacts().
           permutationResult.addArtifacts(EntityRecorder.makeSoycArtifacts(
-              permutationId, sourceInfoMaps, jjsmap, sizeBreakdowns,
+              permutationId, sourceInfoMaps, options.getSourceMapFilePrefix(),
+              jjsmap, sizeBreakdowns,
               ((DependencyGraphRecorder) dependenciesAndRecorder.getRight()), jprogram));
         }
       } else if (isSourceMapsEnabled) {
@@ -402,8 +403,8 @@
               + "compiler.useSourceMaps=true; ignoring compiler.useSourceMaps=true.");
         } else {
           logger.log(TreeLogger.INFO, "Source Maps Enabled");
-          permutationResult.addArtifacts(
-              SourceMapRecorder.makeSourceMapArtifacts(permutationId, sourceInfoMaps));
+          permutationResult.addArtifacts(SourceMapRecorder.exec(permutationId, sourceInfoMaps,
+              options.getSourceMapFilePrefix()));
         }
       }
     }
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/OptionSourceMapFilePrefix.java b/dev/core/src/com/google/gwt/dev/util/arg/OptionSourceMapFilePrefix.java
new file mode 100644
index 0000000..42d4d45
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/OptionSourceMapFilePrefix.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014 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.dev.util.arg;
+
+/**
+ * Specifies a prefix that a debugger should add to the beginning of each
+ * filename in a sourcemap to determine the file's full URL.
+ * (It's saved to the sourceRoot field in SourceMap spec.)
+ * If null, no prefix is saved and Java filenames are relative
+ * to the sourcemap's URL.
+ */
+public interface OptionSourceMapFilePrefix {
+
+  /**
+   * Returns the prefix to be added (or null for no prefix).
+   */
+  String getSourceMapFilePrefix();
+
+  /**
+   * Sets the prefix. (Null will disable it.)
+   */
+  void setSourceMapFilePrefix(String path);
+}
diff --git a/dev/core/test/com/google/gwt/core/ext/linker/SourceMapTest.java b/dev/core/test/com/google/gwt/core/ext/linker/SourceMapTest.java
index fdce59e..aafe4a6 100644
--- a/dev/core/test/com/google/gwt/core/ext/linker/SourceMapTest.java
+++ b/dev/core/test/com/google/gwt/core/ext/linker/SourceMapTest.java
@@ -16,7 +16,6 @@
 package com.google.gwt.core.ext.linker;
 
 import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.soyc.SourceMapRecorderExt;
 import com.google.gwt.core.ext.soyc.coderef.ClassDescriptor;
 import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor;
 import com.google.gwt.core.ext.soyc.coderef.EntityDescriptor.Fragment;
@@ -239,8 +238,9 @@
       SourceMapConsumerV3 sourceMap = new SourceMapConsumerV3();
       sourceMap.parse(stringContent(sourceMapFile));
       if (firstIteration) {
-        mapping.put((Integer) sourceMap.getExtensions().get(SourceMapRecorderExt.PERMUTATION_EXT),
-            symbolTable);
+        Integer permutationId = (Integer) sourceMap.getExtensions().get("x_gwt_permutation");
+        assertNotNull(permutationId);
+        mapping.put(permutationId, symbolTable);
         firstIteration = false;
       }
       sourceMap.visitMappings(new  SourceMapConsumerV3.EntryVisitor() {
@@ -544,4 +544,4 @@
       Util.recursiveDelete(work, false);
     }
   }
-}
\ No newline at end of file
+}