org.chromium.debug.core/src/org/chromium/debug/core/model/BreakpointSynchronizer.java
changeset 355 8726e95bcbba
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.chromium.debug.core/src/org/chromium/debug/core/model/BreakpointSynchronizer.java	Mon Jun 07 16:51:19 2010 -0700
@@ -0,0 +1,631 @@
+package org.chromium.debug.core.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.chromium.debug.core.ChromiumDebugPlugin;
+import org.chromium.debug.core.ChromiumSourceDirector;
+import org.chromium.sdk.Breakpoint;
+import org.chromium.sdk.CallbackSemaphore;
+import org.chromium.sdk.JavascriptVm;
+import org.chromium.sdk.SyncCallback;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.IBreakpointManager;
+import org.eclipse.debug.core.model.IBreakpoint;
+
+/**
+ * A class responsible for comparing breakpoints in workspace and on remote VM and synchronizing
+ * them in both directions. {@link Direction#RESET_REMOTE} allows several synchronization
+ * jobs to different VMs.
+ */
+public class BreakpointSynchronizer {
+  private final JavascriptVm javascriptVm;
+  private final BreakpointMap.InTargetMap breakpointInTargetMap;
+  private final ChromiumSourceDirector sourceDirector;
+  private final BreakpointHelper breakpointHelper;
+  private final String debugModelId;
+
+  public BreakpointSynchronizer(JavascriptVm javascriptVm,
+      BreakpointMap.InTargetMap breakpointInTargetMap,
+      ChromiumSourceDirector sourceDirector, BreakpointHelper breakpointHelper,
+      String debugModelId) {
+    this.javascriptVm = javascriptVm;
+    this.breakpointInTargetMap = breakpointInTargetMap;
+    this.sourceDirector = sourceDirector;
+    this.breakpointHelper = breakpointHelper;
+    this.debugModelId = debugModelId;
+  }
+
+  /**
+   * Describes a direction the breakpoint synchronization should be performed in.
+   */
+  public enum Direction {
+
+    /**
+     * All breakpoints in remote VM/VMs are cleared/updated/created to conform to breakpoints in
+     * Eclipse workspace.
+     */
+    RESET_REMOTE,
+
+    /**
+     * All breakpoints in local workspace are cleared/updated/created to conform to breakpoints in
+     * remote VM (not applicable for multiple VMs).
+     */
+    RESET_LOCAL,
+
+    /**
+     * Breakpoints are created locally or remotely or tied together so that every breakpoint
+     * has a counterpart on other side.
+     */
+    MERGE
+  }
+
+  /**
+   * Additional interface used by {@link BreakpointSynchronizer}.
+   */
+  public interface BreakpointHelper {
+    /**
+     * Create breakpoint on remote VM (asynchronously) and link it to uiBreakpoint.
+     */
+    void createBreakpointOnRemote(ChromiumLineBreakpoint uiBreakpoint,
+        VmResourceId vmResourceId,
+        CreateCallback createCallback, SyncCallback syncCallback);
+
+    interface CreateCallback {
+      void failure(Exception ex);
+      void success();
+    }
+  }
+
+  public interface Callback {
+    void onDone(IStatus status);
+  }
+
+  /**
+   * The main entry method of the class. Asynchronously performs synchronization job.
+   * TODO(peter.rybin): consider some end-of-job notification for this method and possibly locks
+   */
+  public void syncBreakpoints(Direction direction, Callback callback) {
+    ReportBuilder reportBuilder = new ReportBuilder(direction);
+    StatusBuilder statusBuilder = new StatusBuilder(callback, reportBuilder);
+
+    statusBuilder.plan();
+    Exception ex = null;
+    try {
+      syncBreakpointsImpl(direction, statusBuilder);
+    } catch (RuntimeException e) {
+      ex = e;
+    } finally {
+      statusBuilder.done(ex);
+    }
+  }
+
+  private void syncBreakpointsImpl(final Direction direction, final StatusBuilder statusBuilder) {
+    // Collect the remote breakpoints.
+    Collection<? extends Breakpoint> sdkBreakpoints = readSdkBreakpoints(javascriptVm);
+    // Collect all local breakpoints.
+    Set<ChromiumLineBreakpoint> uiBreakpoints = getUiBreakpoints();
+
+    List<Breakpoint> sdkBreakpoints2 = new ArrayList<Breakpoint>(sdkBreakpoints.size());
+
+    if (direction != Direction.MERGE) {
+      breakpointInTargetMap.clear();
+    }
+
+    // Throw away all already linked breakpoints and put remaining into sdkBreakpoints2 list.
+    for (Breakpoint sdkBreakpoint : sdkBreakpoints) {
+      ChromiumLineBreakpoint uiBreakpoint = breakpointInTargetMap.getUiBreakpoint(sdkBreakpoint);
+      if (uiBreakpoint == null) {
+        // No mapping. Schedule for further processing.
+        sdkBreakpoints2.add(sdkBreakpoint);
+      } else {
+        // There is a live mapping. This set should also contain this breakpoint.
+        uiBreakpoints.remove(uiBreakpoint);
+        statusBuilder.getReportBuilder().increment(ReportBuilder.Property.LINKED);
+      }
+    }
+
+    // Sort all breakpoints by (script_name, line_number).
+    SortedBreakpoints<ChromiumLineBreakpoint> sortedUiBreakpoints =
+        sortBreakpoints(uiBreakpoints, uiBreakpointHandler);
+    SortedBreakpoints<Breakpoint> sortedSdkBreakpoints =
+        sortBreakpoints(sdkBreakpoints2, sdkBreakpointHandler);
+
+    BreakpointMerger breakpointMerger = new BreakpointMerger(direction, breakpointInTargetMap);
+
+    // Find all unlinked breakpoints on both sides.
+    mergeBreakpoints(breakpointMerger, sortedUiBreakpoints, sortedSdkBreakpoints);
+
+    List<Breakpoint> sdkBreakpointsToDelete;
+    List<Breakpoint> sdkBreakpointsToCreate;
+    List<ChromiumLineBreakpoint> uiBreakpointsToDelete;
+    List<ChromiumLineBreakpoint> uiBreakpointsToCreate;
+
+    // Plan actions for all breakpoints without pair.
+    if (direction == Direction.RESET_REMOTE) {
+      sdkBreakpointsToDelete = breakpointMerger.getMissingSdk();
+      sdkBreakpointsToCreate = Collections.emptyList();
+    } else {
+      sdkBreakpointsToCreate = breakpointMerger.getMissingSdk();
+      sdkBreakpointsToDelete = Collections.emptyList();
+    }
+
+    if (direction == Direction.RESET_LOCAL) {
+      uiBreakpointsToDelete = breakpointMerger.getMissingUi();
+      uiBreakpointsToCreate = Collections.emptyList();
+    } else {
+      uiBreakpointsToCreate = breakpointMerger.getMissingUi();
+      uiBreakpointsToDelete = Collections.emptyList();
+    }
+
+    // First delete everything, then create (we may need to re-create some breakpoints, so order
+    // is significant).
+    deteleBreakpoints(sdkBreakpointsToDelete, uiBreakpointsToDelete, statusBuilder);
+    createBreakpoints(sdkBreakpointsToCreate, uiBreakpointsToCreate, statusBuilder);
+  }
+
+  private void deteleBreakpoints(List<Breakpoint> sdkBreakpointsToDelete,
+      List<ChromiumLineBreakpoint> uiBreakpointsToDelete, final StatusBuilder statusBuilder) {
+    for (Breakpoint sdkBreakpoint : sdkBreakpointsToDelete) {
+      final PlannedTaskHelper deleteTaskHelper = new PlannedTaskHelper(statusBuilder);
+      JavascriptVm.BreakpointCallback callback = new JavascriptVm.BreakpointCallback() {
+        public void failure(String errorMessage) {
+          deleteTaskHelper.setException(new Exception(errorMessage));
+        }
+        public void success(Breakpoint breakpoint) {
+          statusBuilder.getReportBuilder().increment(ReportBuilder.Property.DELETED_ON_REMOTE);
+        }
+      };
+      sdkBreakpoint.clear(callback, deleteTaskHelper);
+    }
+    for (ChromiumLineBreakpoint uiBreakpoint : uiBreakpointsToDelete) {
+      ChromiumLineBreakpoint.getIgnoreList().add(uiBreakpoint);
+      try {
+        try {
+          uiBreakpoint.delete();
+        } catch (CoreException e) {
+          throw new RuntimeException(e);
+        }
+      } finally {
+        ChromiumLineBreakpoint.getIgnoreList().remove(uiBreakpoint);
+      }
+      statusBuilder.getReportBuilder().increment(ReportBuilder.Property.DELETED_LOCALLY);
+    }
+  }
+
+  private void createBreakpoints(List<Breakpoint> sdkBreakpointsToCreate,
+      List<ChromiumLineBreakpoint> uiBreakpointsToCreate, final StatusBuilder statusBuilder) {
+    IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
+    for (Breakpoint sdkBreakpoint : sdkBreakpointsToCreate) {
+      Object sourceElement = sourceDirector.getSourceElement(sdkBreakpoint);
+      if (sourceElement instanceof IFile == false) {
+        continue;
+      }
+      // We do not actually support working files for scripts with offset.
+      int script_line_offset = 0;
+      IFile resource = (IFile) sourceElement;
+      ChromiumLineBreakpoint uiBreakpoint;
+      try {
+        uiBreakpoint = ChromiumLineBreakpoint.Helper.createLocal(sdkBreakpoint, breakpointManager,
+            resource, script_line_offset, debugModelId);
+        breakpointInTargetMap.add(sdkBreakpoint, uiBreakpoint);
+      } catch (CoreException e) {
+        throw new RuntimeException(e);
+      }
+      statusBuilder.getReportBuilder().increment(ReportBuilder.Property.CREATED_LOCALLY);
+    }
+    for (ChromiumLineBreakpoint uiBreakpoint : uiBreakpointsToCreate) {
+      VmResourceId vmResourceId = uiBreakpointHandler.getVmResourceId(uiBreakpoint);
+      if (vmResourceId == null) {
+        // Actually we should not get here, because getScript call succeeded before.
+        continue;
+      }
+
+      final PlannedTaskHelper createTaskHelper = new PlannedTaskHelper(statusBuilder);
+      BreakpointHelper.CreateCallback createCallback = new BreakpointHelper.CreateCallback() {
+        public void success() {
+          statusBuilder.getReportBuilder().increment(ReportBuilder.Property.CREATED_ON_REMOTE);
+        }
+        public void failure(Exception ex) {
+          createTaskHelper.setException(ex);
+        }
+      };
+      breakpointHelper.createBreakpointOnRemote(uiBreakpoint, vmResourceId, createCallback,
+          createTaskHelper);
+    }
+  }
+
+  private static class BreakpointMerger extends Merger<ChromiumLineBreakpoint, Breakpoint> {
+    private final Direction direction;
+    private final List<ChromiumLineBreakpoint> missingUi = new ArrayList<ChromiumLineBreakpoint>();
+    private final List<Breakpoint> missingSdk = new ArrayList<Breakpoint>();
+    private final BreakpointMap.InTargetMap breakpointMap;
+
+    BreakpointMerger(Direction direction, BreakpointMap.InTargetMap breakpointMap) {
+      this.direction = direction;
+      this.breakpointMap = breakpointMap;
+    }
+    @Override
+    void both(ChromiumLineBreakpoint v1, Breakpoint v2) {
+      if (direction == Direction.MERGE) {
+        breakpointMap.add(v2, v1);
+      } else {
+        onlyFirst(v1);
+        onlySecond(v2);
+      }
+    }
+    @Override
+    void onlyFirst(ChromiumLineBreakpoint v1) {
+      missingUi.add(v1);
+    }
+    @Override
+    void onlySecond(Breakpoint v2) {
+      missingSdk.add(v2);
+    }
+    List<ChromiumLineBreakpoint> getMissingUi() {
+      return missingUi;
+    }
+    List<Breakpoint> getMissingSdk() {
+      return missingSdk;
+    }
+  }
+
+  /**
+   * A class responsible for creating a summary status of synchronization operation. The status
+   * is created once all asynchronous jobs have finished. Each job first registers itself
+   * via {@link #plan()} method and
+   * later reports its result via {@link #done(Exception)} method.
+   * When the last job is reporting its finishing, the status gets built and sent to
+   * {@link #callback}. If no exceptions were registered,
+   * status contains text report from {@link ReportBuilder}.
+   */
+  private static class StatusBuilder {
+    private final Callback callback;
+    private int plannedNumber = 0;
+    private final List<Exception> exceptions = new ArrayList<Exception>(0);
+    private boolean alreadyReported = false;
+    private final ReportBuilder reportBuilder;
+
+    StatusBuilder(Callback callback, ReportBuilder reportBuilder) {
+      this.callback = callback;
+      this.reportBuilder = reportBuilder;
+    }
+
+    ReportBuilder getReportBuilder() {
+      return reportBuilder;
+    }
+
+    public synchronized void plan() {
+      if (alreadyReported) {
+        throw new IllegalStateException();
+      }
+      plannedNumber++;
+    }
+
+    public void done(Exception ex) {
+      boolean timeToReport = doneImpl(ex);
+      if (timeToReport) {
+        reportResult();
+      }
+    }
+
+    private synchronized boolean doneImpl(Exception ex) {
+      if (ex != null) {
+        exceptions.add(ex);
+      }
+      plannedNumber--;
+      if (plannedNumber == 0) {
+        if (!alreadyReported) {
+          alreadyReported = true;
+          return true;
+        }
+      }
+      return false;
+    }
+
+    private void reportResult() {
+      IStatus status;
+      if (exceptions.isEmpty()) {
+        status = new Status(IStatus.OK, ChromiumDebugPlugin.PLUGIN_ID,
+            "Breakpoint synchronization done: " + reportBuilder.build(), null); //$NON-NLS-1$
+      } else {
+        IStatus[] subStatuses = new IStatus[exceptions.size()];
+        for (int i = 0 ; i < subStatuses.length; i++) {
+          subStatuses[i] = new Status(IStatus.ERROR, ChromiumDebugPlugin.PLUGIN_ID,
+              exceptions.get(i).getMessage(), exceptions.get(i));
+        }
+        status = new MultiStatus(ChromiumDebugPlugin.PLUGIN_ID, IStatus.ERROR, subStatuses,
+            "Breakpoint synchronization errors", null); //$NON-NLS-1$
+      }
+      if (callback != null) {
+        callback.onDone(status);
+      }
+    }
+  }
+
+  private static class PlannedTaskHelper implements SyncCallback {
+    private final StatusBuilder statusBuilder;
+    private volatile Exception exception = null;
+    PlannedTaskHelper(StatusBuilder statusBuilder) {
+      this.statusBuilder = statusBuilder;
+      statusBuilder.plan();
+    }
+    public void callbackDone(RuntimeException e) {
+      if (e != null) {
+        exception = e;
+      }
+      statusBuilder.done(exception);
+    }
+    void setException(Exception ex) {
+      exception = ex;
+    }
+  }
+
+  /**
+   * A class that contains several conunters.
+   */
+  private static class ReportBuilder {
+    enum Property {
+      LINKED,
+      CREATED_LOCALLY,
+      DELETED_LOCALLY,
+      CREATED_ON_REMOTE,
+      DELETED_ON_REMOTE;
+      String getVisibleName() {
+        return toString();
+      }
+    }
+
+    private final Direction direction;
+    private final Map<Property, AtomicInteger> counters;
+
+    ReportBuilder(Direction direction) {
+      this.direction = direction;
+      counters = new EnumMap<Property, AtomicInteger>(Property.class);
+      for (Property property : Property.class.getEnumConstants()) {
+        counters.put(property, new AtomicInteger(0));
+      }
+    }
+
+    public void increment(Property property) {
+      counters.get(property).addAndGet(1);
+    }
+
+    public String build() {
+      StringBuilder builder = new StringBuilder();
+      builder.append("direction=").append(direction); //$NON-NLS-1$
+      for (Map.Entry<Property, AtomicInteger> en : counters.entrySet()) {
+        int number = en.getValue().get();
+        if (number == 0) {
+          continue;
+        }
+        builder.append(" ").append(en.getKey().getVisibleName()); //$NON-NLS-1$
+        builder.append("=").append(number); //$NON-NLS-1$
+      }
+      return builder.toString();
+    }
+  }
+
+  /**
+   * A handler for properties of breakpoint type B that helps reading them.
+   */
+  private static abstract class PropertyHandler<B> {
+    /** @return vm resource name or null */
+    abstract VmResourceId getVmResourceId(B breakpoint);
+    /** @return 0-based number */
+    abstract long getLineNumber(B breakpoint);
+  }
+
+  private final PropertyHandler<ChromiumLineBreakpoint> uiBreakpointHandler =
+      new PropertyHandler<ChromiumLineBreakpoint>() {
+      @Override
+    long getLineNumber(ChromiumLineBreakpoint chromiumLineBreakpoint) {
+      int lineNumber;
+      try {
+        // TODO(peter.rybin): Consider supporting inline scripts here.
+        return chromiumLineBreakpoint.getLineNumber() - 1;
+      } catch (CoreException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override
+    VmResourceId getVmResourceId(ChromiumLineBreakpoint chromiumLineBreakpoint) {
+      IMarker marker = chromiumLineBreakpoint.getMarker();
+      if (marker == null) {
+        return null;
+      }
+      IResource resource = marker.getResource();
+      if (resource instanceof IFile == false) {
+        return null;
+      }
+      IFile file = (IFile) resource;
+      try {
+        return sourceDirector.getReverseSourceLookup().findVmResource(file);
+      } catch (CoreException e) {
+        throw new RuntimeException("Failed to read script name from breakpoint", e); //$NON-NLS-1$
+      }
+    }
+  };
+
+  private static final PropertyHandler<Breakpoint> sdkBreakpointHandler =
+      new PropertyHandler<Breakpoint>() {
+    @Override
+    long getLineNumber(Breakpoint breakpoint) {
+      return breakpoint.getLineNumber();
+    }
+
+    @Override
+    VmResourceId getVmResourceId(Breakpoint breakpoint) {
+      if (breakpoint.getType() == Breakpoint.Type.SCRIPT_NAME) {
+        return VmResourceId.forName(breakpoint.getScriptName());
+      } else {
+        Long scriptId = breakpoint.getScriptId();
+        if (scriptId == null) {
+          return null;
+        }
+        return VmResourceId.forId(scriptId);
+      }
+    }
+  };
+
+  /**
+   * A helping structure that holds field of complicated type.
+   */
+  private static class SortedBreakpoints<B> {
+    final Map<VmResourceId, Map<Long, B>> data;
+
+    SortedBreakpoints(Map<VmResourceId, Map<Long, B>> data) {
+      this.data = data;
+    }
+  }
+
+  /**
+   * Put all breakpoints into map script_name -> line_number -> breakpoint.
+   */
+  private static <B> SortedBreakpoints<B> sortBreakpoints(Collection<? extends B> breakpoints,
+      PropertyHandler<B> handler) {
+    Map<VmResourceId, Map<Long, B>> result = new HashMap<VmResourceId, Map<Long, B>>();
+    for (B breakpoint : breakpoints) {
+      VmResourceId vmResourceId = handler.getVmResourceId(breakpoint);
+      if (vmResourceId == null) {
+        continue;
+      }
+      Map<Long, B> subMap = result.get(vmResourceId);
+      if (subMap == null) {
+        subMap = new HashMap<Long, B>(3);
+        result.put(vmResourceId, subMap);
+      }
+      long line = handler.getLineNumber(breakpoint);
+      // For simplicity we ignore multiple breakpoints on the same line.
+      subMap.put(line, breakpoint);
+    }
+    return new SortedBreakpoints<B>(result);
+  }
+
+  /**
+   * A class that implements merge operation for a particular complete/incomplete pair of values.
+   */
+  private static abstract class Merger<V1, V2> {
+    abstract void onlyFirst(V1 v1);
+    abstract void onlySecond(V2 v2);
+    abstract void both(V1 v1, V2 v2);
+  }
+
+  /**
+   * Merges values of 2 maps.
+   * @param map2 must implement {@link Map#remove} method.
+   */
+  private static <K, V1, V2> void mergeMaps(Map<K, V1> map1, Map<K, V2> map2,
+      Merger<V1, V2> merger) {
+    for (Map.Entry<K, V1> en : map1.entrySet()) {
+      V2 v2 = map2.remove(en.getKey());
+      if (v2 == null) {
+        merger.onlyFirst(en.getValue());
+      } else {
+        merger.both(en.getValue(), v2);
+      }
+    }
+    for (V2 v2 : map2.values()) {
+      merger.onlySecond(v2);
+    }
+  }
+
+  private static <B1, B2> void mergeBreakpoints(final Merger<B1, B2> perBreakpointMerger,
+      SortedBreakpoints<B1> side1, SortedBreakpoints<B2> side2) {
+    Merger<Map<Long, B1>, Map<Long, B2>> perScriptMerger =
+        new Merger<Map<Long,B1>, Map<Long,B2>>() {
+      @Override
+      void both(Map<Long, B1> v1, Map<Long, B2> v2) {
+        mergeMaps(v1, v2, perBreakpointMerger);
+      }
+
+      @Override
+      void onlyFirst(Map<Long, B1> v1) {
+        mergeMaps(v1, Collections.<Long, B2>emptyMap(), perBreakpointMerger);
+      }
+
+      @Override
+      void onlySecond(Map<Long, B2> v2) {
+        mergeMaps(Collections.<Long, B1>emptyMap(), v2, perBreakpointMerger);
+      }
+    };
+    mergeMaps(side1.data, side2.data, perScriptMerger);
+  }
+
+
+  private static Collection<? extends Breakpoint> readSdkBreakpoints(JavascriptVm javascriptVm) {
+    class CallbackImpl implements JavascriptVm.ListBreakpointsCallback {
+      public void failure(Exception exception) {
+        problem = exception;
+      }
+
+      public void success(Collection<? extends Breakpoint> breakpoints) {
+        result = breakpoints;
+      }
+      Collection<? extends Breakpoint> getResult() {
+        if (problem != null) {
+          throw new RuntimeException("Failed to synchronize breakpoints", problem); //$NON-NLS-1$
+        }
+        return result;
+      }
+      Exception problem = null;
+      Collection<? extends Breakpoint> result = null;
+    }
+
+    CallbackImpl callback = new CallbackImpl();
+    CallbackSemaphore callbackSemaphore = new CallbackSemaphore();
+
+    javascriptVm.listBreakpoints(callback, callbackSemaphore);
+    boolean res = callbackSemaphore.tryAcquireDefault();
+    if (!res) {
+      throw new RuntimeException("Timeout"); //$NON-NLS-1$
+    }
+
+    return callback.getResult();
+  }
+
+  // We need this method to return Set for future purposes.
+  private Set<ChromiumLineBreakpoint> getUiBreakpoints() {
+    IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
+    Set<ChromiumLineBreakpoint> result = new HashSet<ChromiumLineBreakpoint>();
+
+    for (IBreakpoint breakpoint: breakpointManager.getBreakpoints(debugModelId)) {
+      if (breakpoint instanceof ChromiumLineBreakpoint == false) {
+        continue;
+      }
+      ChromiumLineBreakpoint chromiumLineBreakpoint = (ChromiumLineBreakpoint) breakpoint;
+      result.add(chromiumLineBreakpoint);
+    }
+    return result;
+  }
+
+  public static class ProtocolNotSupportedOnRemote extends Exception {
+    ProtocolNotSupportedOnRemote() {
+    }
+    ProtocolNotSupportedOnRemote(String message, Throwable cause) {
+      super(message, cause);
+    }
+    ProtocolNotSupportedOnRemote(String message) {
+      super(message);
+    }
+    ProtocolNotSupportedOnRemote(Throwable cause) {
+      super(cause);
+    }
+  }
+}