]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5528 Improve performance of syntax highlighting API
authorJulien HENRY <julien.henry@sonarsource.com>
Mon, 25 Aug 2014 16:03:54 +0000 (18:03 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Mon, 25 Aug 2014 16:04:52 +0000 (18:04 +0200)
sonar-batch/src/main/java/org/sonar/batch/highlighting/SyntaxHighlightingData.java
sonar-batch/src/main/java/org/sonar/batch/highlighting/SyntaxHighlightingDataBuilder.java
sonar-batch/src/test/java/org/sonar/batch/highlighting/SyntaxHighlightingDataBuilderTest.java
sonar-batch/src/test/java/org/sonar/batch/mediumtest/highlighting/HighlightingMediumTest.java
sonar-batch/src/test/java/org/sonar/batch/source/DefaultHighlightableTest.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/highlighting/HighlightingBuilder.java

index 56c9b795ac940cd2397f3f7bb500c671debd3048..8e4f5314ec63377d7d4916cf6ab1356e2d56dd90 100644 (file)
@@ -21,29 +21,28 @@ package org.sonar.batch.highlighting;
 
 import org.sonar.batch.index.Data;
 
-import java.util.List;
+import java.util.ArrayList;
+import java.util.Collection;
 
 public class SyntaxHighlightingData implements Data {
 
   private static final String FIELD_SEPARATOR = ",";
   private static final String RULE_SEPARATOR = ";";
 
-  private List<SyntaxHighlightingRule> syntaxHighlightingRuleSet;
+  private Collection<SyntaxHighlightingRule> syntaxHighlightingRuleSet;
 
-  public SyntaxHighlightingData(List<SyntaxHighlightingRule> syntaxHighlightingRuleSet) {
-    this.syntaxHighlightingRuleSet = syntaxHighlightingRuleSet;
+  public SyntaxHighlightingData(Collection<SyntaxHighlightingRule> syntaxHighlightingRuleSet) {
+    this.syntaxHighlightingRuleSet = new ArrayList<SyntaxHighlightingRule>(syntaxHighlightingRuleSet);
   }
 
-  public List<SyntaxHighlightingRule> syntaxHighlightingRuleSet() {
+  public Collection<SyntaxHighlightingRule> syntaxHighlightingRuleSet() {
     return syntaxHighlightingRuleSet;
   }
 
   @Override
   public String writeString() {
     StringBuilder sb = new StringBuilder();
-    List<SyntaxHighlightingRule> orderedRules = syntaxHighlightingRuleSet;
-
-    for (SyntaxHighlightingRule highlightingRule : orderedRules) {
+    for (SyntaxHighlightingRule highlightingRule : syntaxHighlightingRuleSet) {
       sb.append(highlightingRule.getStartPosition())
         .append(FIELD_SEPARATOR)
         .append(highlightingRule.getEndPosition())
index 3a74dbf48840c1e8fc7afd45584e9374806d1701..792cfb2eb3a62a69ef79883f8edd212837a29ff9 100644 (file)
 package org.sonar.batch.highlighting;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.Ordering;
-import org.slf4j.LoggerFactory;
+import org.elasticsearch.common.collect.Sets;
 
 import javax.annotation.Nullable;
 
-import java.util.Collection;
-import java.util.List;
-
-import static com.google.common.collect.Lists.newArrayList;
+import java.util.Iterator;
+import java.util.Set;
 
 public class SyntaxHighlightingDataBuilder {
 
-  private List<SyntaxHighlightingRule> syntaxHighlightingRuleSet;
+  private Set<SyntaxHighlightingRule> syntaxHighlightingRuleSet;
 
   public SyntaxHighlightingDataBuilder() {
-    syntaxHighlightingRuleSet = newArrayList();
+    syntaxHighlightingRuleSet = Sets.newTreeSet(new Ordering<SyntaxHighlightingRule>() {
+      @Override
+      public int compare(@Nullable SyntaxHighlightingRule left,
+        @Nullable SyntaxHighlightingRule right) {
+        int result = left.getStartPosition() - right.getStartPosition();
+        if (result == 0) {
+          result = left.getEndPosition() - right.getEndPosition();
+        }
+        return result;
+      }
+    });
+  }
+
+  @VisibleForTesting
+  public Set<SyntaxHighlightingRule> getSyntaxHighlightingRuleSet() {
+    return syntaxHighlightingRuleSet;
   }
 
   public SyntaxHighlightingDataBuilder registerHighlightingRule(int startOffset, int endOffset, String typeOfText) {
-    if (ruleConflictsWithExistingRules(startOffset, endOffset)) {
-      String errorMsg = String.format("Cannot register highlighting rule for characters from %s to %s as it " +
-        "overlaps at least one existing rule", startOffset, endOffset);
-      LoggerFactory.getLogger(SyntaxHighlightingDataBuilder.class).error(errorMsg);
-      throw new UnsupportedOperationException(errorMsg);
-    }
     SyntaxHighlightingRule syntaxHighlightingRule = SyntaxHighlightingRule.create(startOffset, endOffset,
       typeOfText);
     this.syntaxHighlightingRuleSet.add(syntaxHighlightingRule);
@@ -54,50 +59,24 @@ public class SyntaxHighlightingDataBuilder {
   }
 
   public SyntaxHighlightingData build() {
-    return new SyntaxHighlightingData(getSortedRules());
+    checkOverlappingBoudaries();
+    return new SyntaxHighlightingData(syntaxHighlightingRuleSet);
   }
 
-  private boolean ruleConflictsWithExistingRules(final int startOffset, final int endOffset) {
-    Collection<SyntaxHighlightingRule> conflictingRules = Collections2
-      .filter(syntaxHighlightingRuleSet, new Predicate<SyntaxHighlightingRule>() {
-        @Override
-        public boolean apply(@Nullable SyntaxHighlightingRule syntaxHighlightingRule) {
-
-          if (syntaxHighlightingRule != null) {
-            boolean overlapsStartBoundary = startOffset < syntaxHighlightingRule.getStartPosition()
-              && endOffset >= syntaxHighlightingRule.getStartPosition() + 1
-              && endOffset < syntaxHighlightingRule.getEndPosition();
-
-            boolean overlapsEndBoundary = startOffset > syntaxHighlightingRule.getStartPosition()
-              && startOffset < syntaxHighlightingRule.getEndPosition()
-              && endOffset > syntaxHighlightingRule.getEndPosition();
-
-            return overlapsStartBoundary || overlapsEndBoundary;
-          }
-          return false;
+  private void checkOverlappingBoudaries() {
+    if (syntaxHighlightingRuleSet.size() > 1) {
+      Iterator<SyntaxHighlightingRule> it = syntaxHighlightingRuleSet.iterator();
+      SyntaxHighlightingRule previous = it.next();
+      while (it.hasNext()) {
+        SyntaxHighlightingRule current = it.next();
+        if (previous.getEndPosition() > current.getStartPosition() && !(previous.getEndPosition() >= current.getEndPosition())) {
+          String errorMsg = String.format("Cannot register highlighting rule for characters from %s to %s as it " +
+            "overlaps at least one existing rule", current.getStartPosition(), current.getEndPosition());
+          throw new IllegalStateException(errorMsg);
         }
-      });
-    return !conflictingRules.isEmpty();
-  }
-
-  @VisibleForTesting
-  public List<SyntaxHighlightingRule> getSortedRules() {
-    Ordering<SyntaxHighlightingRule> ruleOrdering = new Ordering<SyntaxHighlightingRule>() {
-      @Override
-      public int compare(@Nullable SyntaxHighlightingRule left,
-        @Nullable SyntaxHighlightingRule right) {
-        int result;
-        if (left != null && right != null) {
-          result = left.getStartPosition() - right.getStartPosition();
-          if (result == 0) {
-            result = left.getEndPosition() - right.getEndPosition();
-          }
-          return result;
-        }
-        return left != null ? 1 : -1;
+        previous = current;
       }
-    };
-
-    return ruleOrdering.sortedCopy(syntaxHighlightingRuleSet);
+    }
   }
+
 }
index 6fad264c8311bad0bec0b639d4727e45a036b2ea..e511bc388de724ec2888a36f83eed5b03c67885a 100644 (file)
  */
 package org.sonar.batch.highlighting;
 
-
-import org.sonar.batch.highlighting.SyntaxHighlightingDataBuilder;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-import java.util.List;
+import java.util.Collection;
 
 import static org.fest.assertions.Assertions.assertThat;
 
 public class SyntaxHighlightingDataBuilderTest {
 
-  private List<SyntaxHighlightingRule> highlightingRules;
+  private Collection<SyntaxHighlightingRule> highlightingRules;
 
   @Rule
   public ExpectedException throwable = ExpectedException.none();
@@ -48,7 +46,7 @@ public class SyntaxHighlightingDataBuilderTest {
     highlightingDataBuilder.registerHighlightingRule(24, 65, "cppd");
     highlightingDataBuilder.registerHighlightingRule(12, 20, "cd");
 
-    highlightingRules = highlightingDataBuilder.getSortedRules();
+    highlightingRules = highlightingDataBuilder.getSyntaxHighlightingRuleSet();
   }
 
   @Test
@@ -58,17 +56,27 @@ public class SyntaxHighlightingDataBuilderTest {
 
   @Test
   public void should_order_by_start_then_end_offset() throws Exception {
-    assertThat(highlightingRules).onProperty("startPosition").containsExactly(0, 10, 12, 24, 24, 42);
-    assertThat(highlightingRules).onProperty("endPosition").containsExactly(10, 12, 20, 38, 65, 50);
-    assertThat(highlightingRules).onProperty("textType").containsExactly("cd", "k", "cd", "k", "cppd", "k");
+    assertThat(highlightingRules).onProperty("startPosition").containsOnly(0, 10, 12, 24, 24, 42);
+    assertThat(highlightingRules).onProperty("endPosition").containsOnly(10, 12, 20, 38, 65, 50);
+    assertThat(highlightingRules).onProperty("textType").containsOnly("cd", "k", "cd", "k", "cppd", "k");
+  }
+
+  @Test
+  public void should_suport_overlapping() throws Exception {
+    SyntaxHighlightingDataBuilder builder = new SyntaxHighlightingDataBuilder();
+    builder.registerHighlightingRule(0, 15, "k");
+    builder.registerHighlightingRule(8, 12, "cppd");
+    builder.build();
   }
 
   @Test
-  public void should_prevent_rules_overlapping() throws Exception {
-    throwable.expect(UnsupportedOperationException.class);
+  public void should_prevent_boudaries_overlapping() throws Exception {
+    throwable.expect(IllegalStateException.class);
+    throwable.expectMessage("Cannot register highlighting rule for characters from 8 to 15 as it overlaps at least one existing rule");
 
     SyntaxHighlightingDataBuilder builder = new SyntaxHighlightingDataBuilder();
     builder.registerHighlightingRule(0, 10, "k");
     builder.registerHighlightingRule(8, 15, "k");
+    builder.build();
   }
 }
index b9fab7ba450da70de45e4f3af20c6cf790fa3d89..6b0501ec9fca7b128b31ddf92c604e68e6044682 100644 (file)
@@ -23,8 +23,10 @@ import com.google.common.collect.ImmutableMap;
 import org.apache.commons.io.FileUtils;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestName;
 import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.sensor.highlighting.HighlightingBuilder;
 import org.sonar.batch.mediumtest.BatchMediumTester;
@@ -38,9 +40,12 @@ import static org.fest.assertions.Assertions.assertThat;
 
 public class HighlightingMediumTest {
 
-  @org.junit.Rule
+  @Rule
   public TemporaryFolder temp = new TemporaryFolder();
 
+  @Rule
+  public TestName testName = new TestName();
+
   public BatchMediumTester tester = BatchMediumTester.builder()
     .registerPlugin("xoo", new XooPlugin())
     .addDefaultQProfile("xoo", "Sonar Way")
@@ -89,4 +94,46 @@ public class HighlightingMediumTest {
 
   }
 
+  @Test
+  public void computeSyntaxHighlightingOnBigFile() throws IOException {
+
+    File baseDir = temp.newFolder();
+    File srcDir = new File(baseDir, "src");
+    srcDir.mkdir();
+
+    File xooFile = new File(srcDir, "sample.xoo");
+    File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting");
+    FileUtils.write(xooFile, "Sample xoo\ncontent");
+    int chunkSize = 100000;
+    StringBuilder sb = new StringBuilder(16 * chunkSize);
+    for (int i = 0; i < chunkSize; i++) {
+      sb.append(i).append(":").append(i + 1).append(":s\n");
+    }
+    FileUtils.write(xoohighlightingFile, sb.toString());
+
+    long start = System.currentTimeMillis();
+    TaskResult result = tester.newTask()
+      .properties(ImmutableMap.<String, String>builder()
+        .put("sonar.task", "scan")
+        .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+        .put("sonar.projectKey", "com.foo.project")
+        .put("sonar.projectName", "Foo Project")
+        .put("sonar.projectVersion", "1.0-SNAPSHOT")
+        .put("sonar.projectDescription", "Description of Foo Project")
+        .put("sonar.sources", "src")
+        .build())
+      .start();
+    long duration = System.currentTimeMillis() - start;
+    assertDurationLessThan(duration, 20000L);
+
+    InputFile file = result.inputFiles().get(0);
+    assertThat(result.highlightingTypeFor(file, 0)).isEqualTo(HighlightingBuilder.TypeOfText.STRING);
+
+  }
+
+  void assertDurationLessThan(long duration, long maxDuration) {
+    assertThat(duration).as(String.format("Expected less than %d ms, got %d ms", maxDuration, duration)).isLessThanOrEqualTo(maxDuration);
+    System.out.printf("Test %s : %d ms (max allowed is %d)\n", testName.getMethodName(), duration, maxDuration);
+  }
+
 }
index 2f98e07cc7946d225c4b44a721195d8440c3e114..efa8024c6a9d08cf5c7d198746ab5b64b43a8dd1 100644 (file)
@@ -44,7 +44,7 @@ public class DefaultHighlightableTest {
     DefaultHighlightable highlightablePerspective = new DefaultHighlightable(mock(Component.class), null);
     highlightablePerspective.newHighlighting().highlight(0, 10, "k").highlight(20, 30, "cppd");
 
-    assertThat(highlightablePerspective.getHighlightingRules().getSortedRules()).hasSize(2);
+    assertThat(highlightablePerspective.getHighlightingRules().getSyntaxHighlightingRuleSet()).hasSize(2);
   }
 
   @Test
index db8d5c46e9346f698305a03732467fef5b22da03..0acc7848e2249ca12841335e77b3369688cf6f0c 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.api.batch.sensor.highlighting;
 
-
 /**
  * This builder is used to define syntax highlighting (aka code coloration) on files.
  * @since 4.5
@@ -73,6 +72,7 @@ public interface HighlightingBuilder {
 
   /**
    * Call this method only once when your are done with defining highlighting of the file.
+   * @throws IllegalStateException if you have defined overlapping highlighting
    */
   void done();
 }