You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ignite.apache.org by GitBox <gi...@apache.org> on 2018/09/11 12:15:15 UTC

[GitHub] asfgit closed pull request #3: IGNITE-9333 Add statistics page

asfgit closed pull request #3: IGNITE-9333 Add statistics page
URL: https://github.com/apache/ignite-teamcity-bot/pull/3
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
index 9782cbf..d9d8d1d 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITeamcity.java
@@ -33,6 +33,7 @@
 import org.apache.ignite.ci.tcmodel.conf.BuildType;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
@@ -52,6 +53,7 @@
 public interface ITeamcity extends AutoCloseable {
 
     String DEFAULT = "<default>";
+    long DEFAULT_BUILDS_COUNT = 1000;
 
     CompletableFuture<List<BuildType>> getProjectSuites(String projectId);
 
@@ -62,7 +64,17 @@
      * @param branch
      * @return list of builds in historical order, recent builds coming last
      */
-    List<BuildRef> getFinishedBuilds(String projectId, String branch);
+    default List<BuildRef> getFinishedBuilds(String projectId, String branch) {
+        return getFinishedBuilds(projectId, branch, null);
+    };
+
+    /**
+     * @param projectId suite ID (string without spaces)
+     * @param branch
+     * @param cnt builds count
+     * @return list of builds in historical order, recent builds coming last
+     */
+    List<BuildRef> getFinishedBuilds(String projectId, String branch, Long cnt);
 
     /**
      * Includes snapshot dependencies failed builds into list
@@ -71,7 +83,19 @@
      * @param branch branch in TC identification
      * @return list of builds in historical order, recent builds coming last
      */
-    List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch);
+    default List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch){
+        return getFinishedBuilds(projectId, branch, null);
+    };
+
+    /**
+     * Includes snapshot dependencies failed builds into list
+     *
+     * @param projectId suite ID (string without spaces)
+     * @param branch branch in TC identification
+     * @param cnt builds count
+     * @return list of builds in historical order, recent builds coming last
+     */
+    List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch, Long cnt);
 
     /**   */
     CompletableFuture<List<BuildRef>> getRunningBuilds(@Nullable String branch);
@@ -80,7 +104,11 @@
     CompletableFuture<List<BuildRef>> getQueuedBuilds(@Nullable String branch);
 
     default int[] getBuildNumbersFromHistory(String projectId, String branchNameForHist) {
-        return getFinishedBuilds(projectId, branchNameForHist).stream().mapToInt(BuildRef::getId).toArray();
+        return getBuildNumbersFromHistory(projectId, branchNameForHist, null);
+    }
+
+    default int[] getBuildNumbersFromHistory(String projectId, String branchNameForHist, Long cnt) {
+        return getFinishedBuilds(projectId, branchNameForHist, cnt).stream().mapToInt(BuildRef::getId).toArray();
     }
 
     Build getBuild(String href);
@@ -114,6 +142,8 @@ default Build getBuild(int id) {
 
     ChangesList getChangesList(String href);
 
+    IssuesUsagesList getIssuesUsagesList(String href);
+
     /**
      * Runs deep collection of all related statistics for particular build
      *
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
index 99f61bb..126ad98 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgnitePersistentTeamcity.java
@@ -36,6 +36,7 @@
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import javax.annotation.Nullable;
@@ -57,6 +58,7 @@
 import org.apache.ignite.ci.tcmodel.conf.BuildType;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrence;
@@ -84,6 +86,7 @@
     public static final String LOG_CHECK_RESULT = "logCheckResult";
     public static final String CHANGE_INFO_FULL = "changeInfoFull";
     public static final String CHANGES_LIST = "changesList";
+    public static final String ISSUES_USAGES_LIST = "issuesUsagesList";
     public static final String TEST_FULL = "testFull";
     public static final String BUILD_PROBLEMS = "buildProblems";
     public static final String BUILD_STATISTICS = "buildStatistics";
@@ -197,7 +200,7 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
     }
 
     /**
-     * @return Build history: {@link BuildRef} lists cache, 32 parts, transaactional
+     * @return Build history: {@link BuildRef} lists cache, 32 parts, transactional
      */
     public IgniteCache<SuiteInBranch, Expirable<List<BuildRef>>> buildHistIncFailedCache() {
         return getOrCreateCacheV2Tx(ignCacheNme(BUILD_HIST_FINISHED_OR_FAILED));
@@ -240,14 +243,15 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
         return loaded;
     }
 
-    private <K, V> V timedLoadIfAbsentOrMerge(IgniteCache<K, Expirable<V>> cache, int seconds, K key,
+    private <K, V> V timedLoadIfAbsentOrMerge(IgniteCache<K, Expirable<V>> cache, int seconds, Long cnt, K key,
         BiFunction<K, V, V> loadWithMerge) {
         @Nullable final Expirable<V> persistedBuilds = cache.get(key);
 
         int fields = ObjectInterner.internFields(persistedBuilds);
 
         if (persistedBuilds != null) {
-            if (persistedBuilds.isAgeLessThanSecs(seconds))
+            if (persistedBuilds.isAgeLessThanSecs(seconds) &&
+                (cnt == null || persistedBuilds.hasCounterGreaterThan(cnt)))
                 return persistedBuilds.getData();
         }
 
@@ -258,7 +262,7 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
         try {
             apply = loadWithMerge.apply(key, persistedBuilds != null ? persistedBuilds.getData() : null);
 
-            final Expirable<V> newVal = new Expirable<>(System.currentTimeMillis(), apply);
+            final Expirable<V> newVal = new Expirable<>(System.currentTimeMillis(), ((List)apply).size(), apply);
 
             cache.put(key, newVal);
         }
@@ -270,14 +274,14 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
     }
 
     /** {@inheritDoc} */
-    @Override public List<BuildRef> getFinishedBuilds(String projectId, String branch) {
+    @Override public List<BuildRef> getFinishedBuilds(String projectId, String branch, Long cnt) {
         final SuiteInBranch suiteInBranch = new SuiteInBranch(projectId, branch);
 
-        return timedLoadIfAbsentOrMerge(buildHistCache(), 60, suiteInBranch,
+        List<BuildRef> buildRefs = timedLoadIfAbsentOrMerge(buildHistCache(), 60, cnt, suiteInBranch,
             (key, persistedValue) -> {
                 List<BuildRef> builds;
                 try {
-                    builds = teamcity.getFinishedBuilds(projectId, branch);
+                    builds = teamcity.getFinishedBuilds(projectId, branch, cnt);
                 }
                 catch (Exception e) {
                     if (Throwables.getRootCause(e) instanceof FileNotFoundException) {
@@ -290,6 +294,13 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
 
                 return mergeByIdToHistoricalOrder(persistedValue, builds);
             });
+
+        if (cnt == null)
+            return buildRefs;
+
+        return buildRefs.stream()
+            .skip(cnt < buildRefs.size() ? buildRefs.size() - cnt : 0)
+            .collect(Collectors.toList());
     }
 
     @NotNull
@@ -307,12 +318,12 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
     //loads build history with following parameter: defaultFilter:false,state:finished
 
     /** {@inheritDoc} */
-    @Override public List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch) {
+    @Override public List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch, Long cnt) {
         final SuiteInBranch suiteInBranch = new SuiteInBranch(projectId, branch);
 
-        return timedLoadIfAbsentOrMerge(buildHistIncFailedCache(), 60, suiteInBranch,
+        return timedLoadIfAbsentOrMerge(buildHistIncFailedCache(), 60, cnt, suiteInBranch,
             (key, persistedValue) -> {
-                List<BuildRef> failed = teamcity.getFinishedBuildsIncludeSnDepFailed(projectId, branch);
+                List<BuildRef> failed = teamcity.getFinishedBuildsIncludeSnDepFailed(projectId, branch, cnt);
 
                 return mergeByIdToHistoricalOrder(persistedValue, failed);
             });
@@ -335,8 +346,8 @@ private IgnitePersistentTeamcity(Ignite ignite, IgniteTeamcityHelper teamcity) {
 
     public static int getTriggerRelCacheValidSecs(int defaultSecs) {
         long msSinceTrigger = System.currentTimeMillis() - lastTriggerMs;
-        long secondsSinceTigger = TimeUnit.MILLISECONDS.toSeconds(msSinceTrigger);
-        return Math.min((int)secondsSinceTigger, defaultSecs);
+        long secondsSinceTrigger = TimeUnit.MILLISECONDS.toSeconds(msSinceTrigger);
+        return Math.min((int)secondsSinceTrigger, defaultSecs);
     }
 
     /** {@inheritDoc} */
@@ -446,18 +457,22 @@ private Build realLoadBuild(String href1) {
 
     /** {@inheritDoc}*/
     @Override public ProblemOccurrences getProblems(Build build) {
-        String href = build.problemOccurrences.href;
+        if (build.problemOccurrences != null) {
+            String href = build.problemOccurrences.href;
 
-        return loadIfAbsent(
-            buildProblemsCache(),
-            href,
-            k -> {
-                ProblemOccurrences problems = teamcity.getProblems(build);
+            return loadIfAbsent(
+                buildProblemsCache(),
+                href,
+                k -> {
+                    ProblemOccurrences problems = teamcity.getProblems(build);
 
-                registerCriticalBuildProblemInStat(build, problems);
+                    registerCriticalBuildProblemInStat(build, problems);
 
-                return problems;
-            });
+                    return problems;
+                });
+        }
+        else
+            return new ProblemOccurrences();
     }
 
     private void registerCriticalBuildProblemInStat(Build build, ProblemOccurrences problems) {
@@ -591,6 +606,24 @@ private void addTestOccurrencesToStat(TestOccurrences val, String normalizedBran
         });
     }
 
+    @Override public IssuesUsagesList getIssuesUsagesList(String href) {
+        IssuesUsagesList issuesUsages =  loadIfAbsentV2(ISSUES_USAGES_LIST, href, href1 -> {
+            try {
+                return teamcity.getIssuesUsagesList(href1);
+            }
+            catch (Exception e) {
+                if (Throwables.getRootCause(e) instanceof FileNotFoundException) {
+                    System.err.println("Issues Usage List not found for href : " + href);
+
+                    return new IssuesUsagesList();
+                }
+                else
+                    throw e;
+            }
+        });
+        return issuesUsages;
+    }
+
     /** {@inheritDoc} */
     @Override public List<RunStat> topTestFailing(int cnt) {
         return CollectionUtil.top(allTestAnalysis(), cnt, Comparator.comparing(RunStat::getFailRate));
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityHelper.java
index 079c80b..d3421b7 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/IgniteTeamcityHelper.java
@@ -62,6 +62,7 @@
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.hist.Builds;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.issues.IssuesUsagesList;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrences;
 import org.apache.ignite.ci.tcmodel.result.stat.Statistics;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
@@ -354,18 +355,28 @@ public String basicAuthToken() {
     private List<BuildRef> getBuildHistory(@Nullable String buildTypeId,
         @Nullable String branchName,
         boolean dfltFilter,
-        @Nullable String state) {
+        @Nullable String state){
+
+        return getBuildHistory(buildTypeId, branchName, dfltFilter, state, null);
+    }
+
+    private List<BuildRef> getBuildHistory(@Nullable String buildTypeId,
+        @Nullable String branchName,
+        boolean dfltFilter,
+        @Nullable String state,
+        @Nullable Long cnt) {
         String btFilter = isNullOrEmpty(buildTypeId) ? "" : ",buildType:" + buildTypeId + "";
         String stateFilter = isNullOrEmpty(state) ? "" : (",state:" + state);
-        String brachFilter = isNullOrEmpty(branchName) ? "" :",branch:" + branchName;
+        String branchFilter = isNullOrEmpty(branchName) ? "" :",branch:" + branchName;
+        long cntFilter = cnt == null ? DEFAULT_BUILDS_COUNT : cnt;
 
         return sendGetXmlParseJaxb(host + "app/rest/latest/builds"
             + "?locator="
             + "defaultFilter:" + dfltFilter
             + btFilter
             + stateFilter
-            + brachFilter
-            + ",count:1000", Builds.class).getBuildsNonNull();
+            + branchFilter
+            + ",count:" + cntFilter, Builds.class).getBuildsNonNull();
     }
 
     public BuildTypeFull getBuildType(String buildTypeId) {
@@ -378,7 +389,16 @@ public Build getBuild(String href) {
     }
 
     public ProblemOccurrences getProblems(Build build) {
-        return getJaxbUsingHref(build.problemOccurrences.href, ProblemOccurrences.class);
+        if (build.problemOccurrences != null) {
+            ProblemOccurrences problemOccurrences = getJaxbUsingHref(build.problemOccurrences.href, ProblemOccurrences.class);
+
+            problemOccurrences.problemOccurrences
+                .forEach(p -> p.buildRef = build);
+
+            return problemOccurrences;
+        }
+        else
+            return new ProblemOccurrences();
     }
 
     public TestOccurrences getTests(String href, String normalizedBranch) {
@@ -401,6 +421,8 @@ public ChangesList getChangesList(String href) {
         return getJaxbUsingHref(href, ChangesList.class);
     }
 
+    public IssuesUsagesList getIssuesUsagesList(String href) { return getJaxbUsingHref(href, IssuesUsagesList.class); }
+
     private <T> T getJaxbUsingHref(String href, Class<T> elem) {
         return sendGetXmlParseJaxb(host + (href.startsWith("/") ? href.substring(1) : href), elem);
     }
@@ -411,17 +433,29 @@ public ChangesList getChangesList(String href) {
     /** {@inheritDoc} */
     @Override public List<BuildRef> getFinishedBuilds(String projectId,
         String branch) {
+
+        return getFinishedBuilds(projectId, branch, null);
+    }
+
+    /** {@inheritDoc} */
+    public List<BuildRef> getFinishedBuilds(String projectId,
+        String branch, Long cnt) {
         List<BuildRef> finished = getBuildHistory(projectId,
             UrlUtil.escape(branch),
             true,
-            null);
+            null, cnt);
 
         return finished.stream().filter(BuildRef::isNotCancelled).collect(Collectors.toList());
     }
 
     /** {@inheritDoc} */
     @Override public List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch) {
-        return getBuildsInState(projectId, branch, BuildRef.STATE_FINISHED);
+        return getBuildsInState(projectId, branch, BuildRef.STATE_FINISHED, null);
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<BuildRef> getFinishedBuildsIncludeSnDepFailed(String projectId, String branch, Long cnt) {
+        return getBuildsInState(projectId, branch, BuildRef.STATE_FINISHED, cnt);
     }
 
     /** {@inheritDoc} */
@@ -437,14 +471,23 @@ public ChangesList getChangesList(String href) {
     private List<BuildRef> getBuildsInState(
         @Nullable final String projectId,
         @Nullable final String branch,
-        @Nonnull final String state) {
+        @Nonnull final String state,
+        @Nullable final Long cnt) {
         List<BuildRef> finished = getBuildHistory(projectId,
             UrlUtil.escape(branch),
             false,
-            state);
+            state, cnt);
         return finished.stream().filter(BuildRef::isNotCancelled).collect(Collectors.toList());
     }
 
+    private List<BuildRef> getBuildsInState(
+        @Nullable final String projectId,
+        @Nullable final String branch,
+        @Nonnull final String state) {
+
+        return getBuildsInState(projectId, branch, state, null);
+    }
+
     public String serverId() {
         return tcName;
     }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/Expirable.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/Expirable.java
index 16a9f2b..32d176a 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/Expirable.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/Expirable.java
@@ -27,14 +27,16 @@
 public class Expirable<D> {
     private final long ts;
     private final D data;
+    private final long cnt;
 
     public Expirable(D data) {
-        this(System.currentTimeMillis(), data);
+        this(System.currentTimeMillis(), 1, data);
     }
 
-    public Expirable(long ts, D data) {
+    public Expirable(long ts, long cnt, D data) {
         this.ts = ts;
         this.data = data;
+        this.cnt = cnt;
     }
 
     public long getTs() {
@@ -45,6 +47,10 @@ public D getData() {
         return data;
     }
 
+    public long getCnt(){
+        return cnt;
+    }
+
     public long getAgeMs() {
         return System.currentTimeMillis() - ts;
     }
@@ -52,4 +58,8 @@ public long getAgeMs() {
     public boolean isAgeLessThanSecs(int seconds) {
         return getAgeMs() < TimeUnit.SECONDS.toMillis(seconds);
     }
+
+    public boolean hasCounterGreaterThan(long cnt){
+        return cnt < this.cnt;
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/MultBuildRunCtx.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/MultBuildRunCtx.java
index 7b62e23..4a9d47c 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/MultBuildRunCtx.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/analysis/MultBuildRunCtx.java
@@ -125,7 +125,7 @@ public String buildTypeId() {
     public boolean hasNontestBuildProblem() {
         return problems != null && problems.stream().anyMatch(problem ->
             !problem.isFailedTests()
-                && !problem.isShaphotDepProblem()
+                && !problem.isSnapshotDepProblem()
                 && !ProblemOccurrence.BUILD_FAILURE_ON_MESSAGE.equals(problem.type));
         //todo what to do with BuildFailureOnMessage, now it is ignored
     }
@@ -135,7 +135,7 @@ public boolean hasAnyBuildProblemExceptTestOrSnapshot() {
     }
 
     private Optional<ProblemOccurrence> getBuildProblemExceptTestOrSnapshot() {
-        return problems.stream().filter(p -> !p.isFailedTests() && !p.isShaphotDepProblem()).findAny();
+        return problems.stream().filter(p -> !p.isFailedTests() && !p.isSnapshotDepProblem()).findAny();
     }
 
     public List<SingleBuildRunCtx> getBuilds() {
@@ -244,7 +244,7 @@ public String getResult() {
             Stream<ProblemOccurrence> stream =
                 problems.stream().filter(p ->
                     !p.isFailedTests()
-                        && !p.isShaphotDepProblem()
+                        && !p.isSnapshotDepProblem()
                         && !p.isExecutionTimeout()
                         && !p.isJvmCrash()
                         && !p.isExitCode()
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/IssueRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/ProblemRef.java
similarity index 93%
rename from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/IssueRef.java
rename to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/ProblemRef.java
index d64ddf6..6917bf6 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/IssueRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/issue/ProblemRef.java
@@ -17,11 +17,11 @@
 
 package org.apache.ignite.ci.issue;
 
-public class IssueRef {
+public class ProblemRef {
     public String name;
     public String webUrl;
 
-    public IssueRef(String name) {
+    public ProblemRef(String name) {
         this.name = name;
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
index 5022985..0e6202b 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
@@ -52,6 +52,8 @@
 
     @XmlAttribute public Boolean composite;
 
+    @XmlAttribute public String webUrl;
+
     /**
      * @return Build ID
      */
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Build.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Build.java
index ece1c8f..9c6a9cd 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Build.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/Build.java
@@ -58,6 +58,8 @@
 
     @XmlElement(name = "statistics") public StatisticsRef statisticsRef;
 
+    @XmlElement(name = "relatedIssues") public RelatedIssuesRef relatedIssuesRef;
+
     /** Changes not included into build.*/
     @XmlElement(name = "lastChanges") public ChangesList lastChanges;
 
@@ -87,11 +89,19 @@ public String getFinishDateDdMmYyyy() throws ParseException {
     }
 
     public Date getFinishDate() {
+        return getDate(finishDate);
+    }
+
+    public Date getStartDate() {
+        return getDate(startDate);
+    }
+
+    private Date getDate(String date) {
         try {
-            if (finishDate == null)
+            if (date == null)
                 return null;
             SimpleDateFormat f = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
-            return f.parse(finishDate);
+            return f.parse(date);
         }
         catch (ParseException e) {
             throw new IllegalStateException(e);
@@ -127,4 +137,4 @@ public Triggered getTriggered() {
     public void setTriggered(Triggered triggered) {
         this.triggered = triggered;
     }
-}
+}
\ No newline at end of file
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/ProblemOccurrencesRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/ProblemOccurrencesRef.java
index 7bc3ff1..a382ddb 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/ProblemOccurrencesRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/ProblemOccurrencesRef.java
@@ -25,4 +25,8 @@
 public class ProblemOccurrencesRef {
     /** Href without host name to obtain full problems list. */
     @XmlAttribute public String href;
+
+    @XmlAttribute public long count;
+
+    @XmlAttribute public long newFailed;
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/RelatedIssuesRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/RelatedIssuesRef.java
new file mode 100644
index 0000000..81e1157
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/RelatedIssuesRef.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.tcmodel.result;
+
+/**
+ * Related issues reference.
+ */
+public class RelatedIssuesRef extends AbstractRef {
+}
+
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/TestOccurrencesRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/TestOccurrencesRef.java
index 6cbda4a..b250e98 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/TestOccurrencesRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/TestOccurrencesRef.java
@@ -29,5 +29,7 @@
     @XmlAttribute public Integer count;
     @XmlAttribute public Integer passed;
     @XmlAttribute public Integer failed;
+    @XmlAttribute public Integer newFailed;
+    @XmlAttribute public Integer ignored;
     @XmlAttribute public Integer muted;
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueRef.java
new file mode 100644
index 0000000..09ea91a
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueRef.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.tcmodel.result.issues;
+
+import javax.xml.bind.annotation.XmlAttribute;
+
+/**
+ * Issue short version from list of build's related issues.
+ *
+ * See example of XML, e.g. here
+ * https://ci.ignite.apache.org/app/rest/latest/builds/id:1694977/relatedIssues
+ */
+public class IssueRef {
+    @XmlAttribute public String id;
+    @XmlAttribute public String url;
+
+    @Override public String toString() {
+        return "IssueRef{" +
+            "id='" + id + '\'' +
+            ", url='" + url + '\'' +
+            '}';
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueUsage.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueUsage.java
new file mode 100644
index 0000000..ae57c4d
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssueUsage.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.tcmodel.result.issues;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.apache.ignite.ci.tcmodel.changes.ChangesList;
+
+/**
+ * Build's related issue from TC.
+ *
+ * See example of XML, e.g. here
+ * https://ci.ignite.apache.org/app/rest/latest/builds/id:1694977/relatedIssues
+ */
+@XmlRootElement(name = "IssueUsage")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class IssueUsage {
+    @XmlElement(name = "issue")
+    private IssueRef issue;
+    @XmlElement(name = "changes")
+    private ChangesList changesList;
+
+    public IssueRef getIssue(){
+        return issue;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssuesUsagesList.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssuesUsagesList.java
new file mode 100644
index 0000000..1212ab6
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/issues/IssuesUsagesList.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.tcmodel.result.issues;
+
+import java.util.Collections;
+import java.util.List;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * List of build's related issues from TC.
+ *
+ * See example of XML, e.g. here
+ * https://ci.ignite.apache.org/app/rest/latest/builds/id:1694977/relatedIssues
+ */
+@XmlRootElement(name = "issuesUsages")
+public class IssuesUsagesList {
+    @XmlElement(name = "issueUsage")
+    private List<IssueUsage> issuesUsages;
+
+    @XmlElement Integer count;
+
+    @XmlElement String href;
+
+    public List<IssueUsage> getIssuesUsagesNonNull() {
+        return issuesUsages == null ? Collections.emptyList() : issuesUsages;
+    }
+
+    @Override public String toString() {
+        return "IssuesUsagesList{" +
+            "issuesUsages=" + issuesUsages +
+            '}';
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrence.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrence.java
index 92868bc..b8ae4f0 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrence.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrence.java
@@ -18,34 +18,41 @@
 package org.apache.ignite.ci.tcmodel.result.problems;
 
 import javax.xml.bind.annotation.XmlAttribute;
+import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 
 /**
  * One build problem. Contains its type.
  */
 public class ProblemOccurrence {
     public static final String BUILD_FAILURE_ON_MESSAGE = "BuildFailureOnMessage";
-    private static final String TC_EXIT_CODE = "TC_EXIT_CODE";
-    private static final String TC_OOME = "TC_OOME";
+    public static final String TC_EXIT_CODE = "TC_EXIT_CODE";
+    public static final String TC_OOME = "TC_OOME";
+    public static final String TC_EXECUTION_TIMEOUT = "TC_EXECUTION_TIMEOUT";
+    public static final String TC_FAILED_TESTS = "TC_FAILED_TESTS";
+    public static final String TC_JVM_CRASH = "TC_JVM_CRASH";
+    public static final String OTHER = "OTHER";
+    public static final String SNAPSHOT_DEPENDENCY_ERROR = "SNAPSHOT_DEPENDENCY_ERROR_BUILD_PROCEEDS_TYPE";
 
     @XmlAttribute public String id;
     @XmlAttribute public String identity;
     @XmlAttribute public String type;
     @XmlAttribute public String href;
+    public BuildRef buildRef;
 
     public boolean isExecutionTimeout() {
-        return "TC_EXECUTION_TIMEOUT".equals(type);
+        return TC_EXECUTION_TIMEOUT.equals(type);
     }
 
     public boolean isFailedTests() {
-        return "TC_FAILED_TESTS".equals(type);
+        return TC_FAILED_TESTS.equals(type);
     }
 
-    public boolean isShaphotDepProblem() {
-        return "SNAPSHOT_DEPENDENCY_ERROR_BUILD_PROCEEDS_TYPE".equals(type);
+    public boolean isSnapshotDepProblem() {
+        return SNAPSHOT_DEPENDENCY_ERROR.equals(type);
     }
 
     public boolean isJvmCrash() {
-        return "TC_JVM_CRASH".equals(type);
+        return TC_JVM_CRASH.equals(type);
     }
 
     public boolean isOome() {
@@ -55,4 +62,13 @@ public boolean isOome() {
     public boolean isExitCode() {
         return TC_EXIT_CODE.equals(type);
     }
+
+    public boolean isOther(){
+        return !isFailedTests()
+            && !isSnapshotDepProblem()
+            && !isExecutionTimeout()
+            && !isJvmCrash()
+            && !isExitCode()
+            && !isOome();
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrences.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrences.java
index 43166d4..c95a7bc 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrences.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/result/problems/ProblemOccurrences.java
@@ -29,9 +29,10 @@
 @XmlRootElement(name = "problemOccurrences")
 public class ProblemOccurrences extends ProblemOccurrencesRef {
     @XmlElement(name = "problemOccurrence")
-    private List<ProblemOccurrence> problemOccurrences;
+    public List<ProblemOccurrence> problemOccurrences;
 
     public List<ProblemOccurrence> getProblemsNonNull() {
         return problemOccurrences == null ? Collections.emptyList() : problemOccurrences;
     }
 }
+
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
new file mode 100644
index 0000000..2b73771
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.web.model.current;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nonnull;
+import org.apache.ignite.ci.ITeamcity;
+import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.tcmodel.result.issues.IssueRef;
+import org.apache.ignite.ci.tcmodel.result.issues.IssueUsage;
+import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
+import org.apache.ignite.ci.util.TimeUtil;
+import org.apache.ignite.ci.web.IBackgroundUpdatable;
+
+/**
+ * Summary of build statistics.
+ */
+public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpdatable {
+    /** Build with test and problems references. */
+    public Build build;
+
+    /** List of problem occurrences. */
+    private List<ProblemOccurrence> problems;
+
+    /** List of related issues. */
+    public List<IssueRef> relatedIssues;
+
+    /** Duration printable. */
+    public String durationPrintable;
+
+    /** Short build run result (without snapshot-dependencies printable result). */
+    public String shortRes;
+
+    /** Snapshot-dependencies build run result. */
+    public List<String> fullRes;
+
+    /** Snapshot-dependency. */
+    private List<BuildRef> snapshotDependencies;
+
+    /** Build problems count. */
+    public long problemsCount;
+
+    public BuildStatisticsSummary(Build build){
+        this.build = build;
+    }
+
+    /** Initialize build statistics. */
+    public void initialize(@Nonnull final ITeamcity teamcity) {
+
+        if (build.isFakeStub())
+            return;
+
+        relatedIssues = teamcity.getIssuesUsagesList(build.relatedIssuesRef.href).getIssuesUsagesNonNull().stream()
+            .map(IssueUsage::getIssue).collect(Collectors.toList());
+
+        durationPrintable = TimeUtil
+            .getDurationPrintable(build.getFinishDate().getTime() - build.getStartDate().getTime());
+
+        snapshotDependencies = getSnapshotDependencies(teamcity, build);
+
+        problems = getProblems(teamcity);
+
+        shortRes = getShortRes();
+
+        fullRes = getFullRes();
+
+        problemsCount = getAllProblemsCount(null);
+    }
+
+    private long getExecutionTimeoutCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isExecutionTimeout).count();
+    }
+
+    private long getJvmCrashProblemCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isJvmCrash).count();
+    }
+
+    private long getExitCodeProblemsCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isExitCode).count();
+    }
+
+    private long getOomeProblemCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isOome).count();
+    }
+
+    private long getFailedTestsProblemCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isFailedTests).count();
+    }
+
+    private long getSnapshotDepProblemCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isSnapshotDepProblem).count();
+    }
+
+    private long getOtherProblemCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).filter(ProblemOccurrence::isOther).count();
+    }
+
+    private long getAllProblemsCount(String buildTypeId) {
+        return getProblemsStream(buildTypeId).count();
+    }
+
+    /**
+     * Problems for all snapshot-dependencies.
+     *
+     * @param teamcity Teamcity.
+     */
+    private List<ProblemOccurrence> getProblems(@Nonnull final ITeamcity teamcity){
+        if (snapshotDependencies == null)
+            return Collections.emptyList();
+
+        List<ProblemOccurrence> problemOccurrences = new ArrayList<>();
+
+        List<BuildRef> snapshotDependencyWithProblems = getSnapshotDependenciesWithProblems();
+
+        for (BuildRef buildRef : snapshotDependencyWithProblems)
+            problemOccurrences.addAll(teamcity
+                .getProblems(teamcity.getBuild(buildRef.href))
+                .getProblemsNonNull());
+
+        return problemOccurrences;
+    }
+
+    /**
+     * Snapshot-dependencies for build.
+     *
+     * @param teamcity Teamcity.
+     * @param buildRef Build reference.
+     */
+    private List<BuildRef> getSnapshotDependencies(@Nonnull final ITeamcity teamcity, BuildRef buildRef){
+        List<BuildRef> snapshotDependencies = new ArrayList<>();
+
+        if (buildRef.isComposite()){
+            Build build = teamcity.getBuild(buildRef.href);
+
+            for (BuildRef snDep : build.getSnapshotDependenciesNonNull())
+                snapshotDependencies.addAll(getSnapshotDependencies(teamcity, snDep));
+        } else
+            snapshotDependencies.add(buildRef);
+
+        return snapshotDependencies;
+    }
+
+    /**
+     * Snapshot-dependencies without status "Success".
+     */
+    private List<BuildRef> getSnapshotDependenciesWithProblems(){
+        if (snapshotDependencies == null)
+            return Collections.emptyList();
+
+        return snapshotDependencies.stream()
+            .filter(b -> !b.isSuccess())
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * @param buildTypeId Build type id (if null - for all problems).
+     */
+    private Stream<ProblemOccurrence> getProblemsStream(String buildTypeId) {
+        if (problems == null)
+            return Stream.empty();
+
+        return problems.stream()
+            .filter(Objects::nonNull)
+            .filter(p -> buildTypeId == null || buildTypeId.equals(p.buildRef.buildTypeId)
+            );
+    }
+
+    /**
+     * Full build run result (snapshot-dependencies printable result).
+     *
+     * @return printable result;
+     */
+    private List<String> getFullRes(){
+        List<String> fullRes = new ArrayList<>();
+
+        List<BuildRef> snapshotDependencyWithProblems = getSnapshotDependenciesWithProblems();
+
+        for (BuildRef build : snapshotDependencyWithProblems)
+            fullRes.add(getRes(build.buildTypeId));
+
+        return fullRes.stream()
+            .sorted()
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * Short build run result (without snapshot-dependencies printable result).
+     *
+     * @return printable result;
+     */
+    private String getShortRes(){
+        return getRes(null);
+    }
+
+    /**
+     * Build run result for buildTypeId.
+     *
+     * @param buildTypeId buildTypeId.
+     *
+     * @return printable result.
+     */
+    private String getRes(String buildTypeId){
+        StringBuilder res = new StringBuilder();
+
+        addKnownProblemCnt(res, ProblemOccurrence.TC_EXECUTION_TIMEOUT, getExecutionTimeoutCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.TC_JVM_CRASH, getJvmCrashProblemCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.TC_OOME, getOomeProblemCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.TC_EXIT_CODE, getExitCodeProblemsCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.TC_FAILED_TESTS, getFailedTestsProblemCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.SNAPSHOT_DEPENDENCY_ERROR, getSnapshotDepProblemCount(buildTypeId));
+        addKnownProblemCnt(res, ProblemOccurrence.OTHER, getOtherProblemCount(buildTypeId));
+
+        res.insert(0, (buildTypeId != null ? buildTypeId : "TOTAL") + " [" + getAllProblemsCount(buildTypeId) + "]"
+            + (res.length() != 0 ? ": " : " "));
+
+        return res.toString();
+    }
+
+    /**
+     * @param res Response.
+     * @param nme Name of problem.
+     * @param execToCnt Execute to count.
+     */
+    private void addKnownProblemCnt(StringBuilder res, String nme, long execToCnt) {
+        if (execToCnt > 0) {
+            if (res.length() > 0)
+                res.append(", ");
+
+            res.append(nme)
+                .append(execToCnt > 1 ? " [" + execToCnt + "]" : "");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setUpdateRequired(boolean update) {
+        updateRequired = update;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof BuildStatisticsSummary))
+            return false;
+
+        BuildStatisticsSummary that = (BuildStatisticsSummary)o;
+
+        return problemsCount == that.problemsCount &&
+            Objects.equals(build, that.build) &&
+            Objects.equals(problems, that.problems) &&
+            Objects.equals(relatedIssues, that.relatedIssues) &&
+            Objects.equals(durationPrintable, that.durationPrintable) &&
+            Objects.equals(getShortRes(), that.getShortRes()) &&
+            Objects.equals(getFullRes(), that.getFullRes()) &&
+            Objects.equals(snapshotDependencies, that.snapshotDependencies);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+
+        return Objects.hash(build, problems, relatedIssues, durationPrintable, getShortRes(), getFullRes(),
+            snapshotDependencies, problemsCount);
+    }
+}
\ No newline at end of file
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/SuiteCurrentStatus.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/SuiteCurrentStatus.java
index 1a6b6c6..91945c1 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/SuiteCurrentStatus.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/SuiteCurrentStatus.java
@@ -38,7 +38,7 @@
 import org.apache.ignite.ci.analysis.TestInBranch;
 import org.apache.ignite.ci.analysis.TestLogCheckResult;
 import org.apache.ignite.ci.issue.EventTemplates;
-import org.apache.ignite.ci.issue.IssueRef;
+import org.apache.ignite.ci.issue.ProblemRef;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 import org.apache.ignite.ci.web.rest.GetBuildLog;
 import org.jetbrains.annotations.NotNull;
@@ -105,7 +105,7 @@
 
     public String durationPrintable;
 
-    @Nullable public IssueRef problemRef;
+    @Nullable public ProblemRef problemRef;
 
 
     public void initFromContext(@Nonnull final ITeamcity teamcity,
@@ -244,12 +244,12 @@ private void initStat(@Nullable ITcAnalytics tcAnalytics, String failRateNormali
             RunStat.TestId testId = latestRunsSrc.detectTemplate(EventTemplates.newFailure);
 
             if (testId != null)
-                problemRef = new IssueRef("New Failure");
+                problemRef = new ProblemRef("New Failure");
 
             RunStat.TestId buildIdCritical  = latestRunsSrc.detectTemplate(EventTemplates.newCriticalFailure);
 
             if (buildIdCritical != null)
-                problemRef = new IssueRef("New Critical Failure");
+                problemRef = new ProblemRef("New Critical Failure");
         }
     }
 
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/TestFailure.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/TestFailure.java
index df07282..3b91224 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/TestFailure.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/TestFailure.java
@@ -32,7 +32,7 @@
 import org.apache.ignite.ci.analysis.RunStat;
 import org.apache.ignite.ci.analysis.TestInBranch;
 import org.apache.ignite.ci.issue.EventTemplates;
-import org.apache.ignite.ci.issue.IssueRef;
+import org.apache.ignite.ci.issue.ProblemRef;
 import org.apache.ignite.ci.logs.LogMsgToWarn;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrenceFull;
 
@@ -78,7 +78,7 @@
 
     public List<String> warnings = new ArrayList<>();
 
-    @Nullable public IssueRef problemRef;
+    @Nullable public ProblemRef problemRef;
 
     @Nullable public String flakyComments;
 
@@ -209,12 +209,12 @@ public void initStat(@Nullable final Function<TestInBranch, RunStat> runStatSupp
             RunStat.TestId testId = latestRunsSrc.detectTemplate(EventTemplates.newFailure);
 
             if (testId != null)
-                problemRef = new IssueRef("New Failure");
+                problemRef = new ProblemRef("New Failure");
 
             RunStat.TestId recentContributedTestId = latestRunsSrc.detectTemplate(EventTemplates.newContributedTestFailure);
 
             if (recentContributedTestId != null)
-                problemRef = new IssueRef("Recently contributed test failure");
+                problemRef = new ProblemRef("Recently contributed test failure");
 
             flakyComments = latestRunsSrc.getFlakyComments();
         }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
index 7ba09a2..04b9681 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
@@ -17,13 +17,11 @@
 
 package org.apache.ignite.ci.web.rest.build;
 
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.GET;
@@ -36,12 +34,13 @@
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.ITcHelper;
 import org.apache.ignite.ci.ITeamcity;
-import org.apache.ignite.ci.IgnitePersistentTeamcity;
 import org.apache.ignite.ci.analysis.FullChainRunCtx;
 import org.apache.ignite.ci.analysis.mode.LatestRebuildMode;
 import org.apache.ignite.ci.analysis.mode.ProcessLogsMode;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.current.BuildStatisticsSummary;
 import org.apache.ignite.ci.web.rest.login.ServiceUnauthorizedException;
 import org.apache.ignite.ci.web.BackgroundUpdater;
 import org.apache.ignite.ci.web.CtxListener;
@@ -52,11 +51,14 @@
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static com.google.common.base.Strings.isNullOrEmpty;
+
 @Path(GetBuildTestFailures.BUILD)
 @Produces(MediaType.APPLICATION_JSON)
 public class GetBuildTestFailures {
     public static final String BUILD = "build";
     public static final String TEST_FAILURES_SUMMARY_CACHE_NAME = BUILD + "TestFailuresSummary";
+    public static final String BUILDS_STATISTICS_SUMMARY_CACHE_NAME = BUILD + "sStatisticsSummary";
     @Context
     private ServletContext context;
 
@@ -88,7 +90,7 @@ public TestFailuresSummary getBuildTestFails(
         @QueryParam("serverId") String serverId,
         @QueryParam("buildId") Integer buildId,
         @Nullable @QueryParam("checkAllLogs") Boolean checkAllLogs)
-            throws ServiceUnauthorizedException {
+        throws ServiceUnauthorizedException {
 
         final BackgroundUpdater updater = CtxListener.getBackgroundUpdater(context);
 
@@ -131,7 +133,6 @@ public TestFailuresSummary getBuildTestFails(
                     (checkAllLogs != null && checkAllLogs) ? ProcessLogsMode.ALL : ProcessLogsMode.SUITE_NOT_COMPLETE,
                     false, false, teamcity, failRateBranch);
 
-
             pubCtx.ifPresent(ctx -> {
                 final ChainAtServerCurrentStatus chainStatus = new ChainAtServerCurrentStatus(serverId, ctx.branchName());
 
@@ -149,4 +150,67 @@ public TestFailuresSummary getBuildTestFails(
 
         return res;
     }
-}
+
+    @GET
+    @Path("history")
+    public List<BuildStatisticsSummary> getBuildsHistory(
+        @Nullable @QueryParam("server") String server,
+        @Nullable @QueryParam("buildType") String buildType,
+        @Nullable @QueryParam("branch") String branch,
+        @Nullable @QueryParam("count") Long count)
+        throws ServiceUnauthorizedException {
+
+        String srvId = isNullOrEmpty(server) ? "apache" : server;
+        String buildTypeId = isNullOrEmpty(buildType) ? "IgniteTests24Java8_RunAll" : buildType;
+        String branchName = isNullOrEmpty(branch) ? "refs/heads/master" : branch;
+        long cnt = count == null ? 50 : count;
+
+        final BackgroundUpdater updater = CtxListener.getBackgroundUpdater(context);
+
+        final ITcHelper tcHelper = CtxListener.getTcHelper(context);
+
+        final ICredentialsProv prov = ICredentialsProv.get(req);
+
+        try (IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov)) {
+
+            int[] finishedBuilds = teamcity.getBuildNumbersFromHistory(buildTypeId, branchName, cnt);
+
+            List<BuildStatisticsSummary> buildsStatistics = new ArrayList<>();
+
+            for (int i = 0; i < finishedBuilds.length; i++) {
+                int buildId = finishedBuilds[i];
+
+                FullQueryParams param = new FullQueryParams();
+                param.setBuildId(buildId);
+                param.setBranch(branchName);
+                param.setServerId(srvId);
+
+                BuildStatisticsSummary buildsStatistic = updater.get(
+                    BUILDS_STATISTICS_SUMMARY_CACHE_NAME, prov, param,
+                    (k) -> getBuildStatisticsSummaryNoCache(srvId, buildId), false);
+
+                if (!buildsStatistic.build.isFakeStub())
+                    buildsStatistics.add(buildsStatistic);
+            }
+
+            return buildsStatistics;
+        }
+    }
+
+    private BuildStatisticsSummary getBuildStatisticsSummaryNoCache(String server, int buildId) {
+
+        String srvId = isNullOrEmpty(server) ? "apache" : server;
+
+        final ITcHelper tcHelper = CtxListener.getTcHelper(context);
+
+        final ICredentialsProv creds = ICredentialsProv.get(req);
+
+        try (IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, creds)) {
+
+            BuildStatisticsSummary stat = new BuildStatisticsSummary(teamcity.getBuild(buildId));
+            stat.initialize(teamcity);
+
+            return stat;
+        }
+    }
+}
\ No newline at end of file
diff --git a/ignite-tc-helper-web/src/main/webapp/index.html b/ignite-tc-helper-web/src/main/webapp/index.html
index 2466f17..c642863 100644
--- a/ignite-tc-helper-web/src/main/webapp/index.html
+++ b/ignite-tc-helper-web/src/main/webapp/index.html
@@ -6,7 +6,7 @@
     <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
 
     <link rel="stylesheet" href="css/style-1.5.css">
-    
+
     <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
     <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
     <script src="js/common-1.6.js"></script>
@@ -45,7 +45,7 @@
         error: showErrInLoadStatus
     });
 
-     $.ajax({
+    $.ajax({
         url: "rest/branches/getServerIds",
         success: function(result) {
             $("#loadStatus").html("");
@@ -122,7 +122,8 @@
 <a href="restpretty.html?url=top/failingSuite">Top failing suites</a> (JSON) <br>
 <!--<a href="./status">Current Build Status (obsolete)</a><br>-->
 <br>
-
+<a href="statistics.html?buildType=IgniteTests24Java8_RunAll&branch=refs/heads/master&count=50">Master's branch statistics</a><br>
+<br>
 Check branch/PR:   <br>
 <div id="suitesForPrCheck"></div>
 <br>
diff --git a/ignite-tc-helper-web/src/main/webapp/statistics.html b/ignite-tc-helper-web/src/main/webapp/statistics.html
new file mode 100644
index 0000000..ffdf9bb
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/webapp/statistics.html
@@ -0,0 +1,203 @@
+<html>
+<head>
+    <title>Ignite Teamcity - statistics master's branch</title>
+    <link rel="icon" href="img/leaf-icon-png-7066.png">
+    <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+    <link rel="stylesheet" href="css/style-1.5.css">
+
+    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
+    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
+    <script src="js/common-1.6.js"></script>
+    <script src="js/testfails-2.0.js"></script>
+    <style>
+        table {
+            width: 70%;
+            border-collapse: collapse;
+        }
+        td, th {
+
+            padding: 10px 5px 10px 5px;
+        }
+        th {
+            text-align: left;
+            padding: 5px;
+            background-color: #f5f5ff;
+            color: #000000;
+        }
+        tr:nth-child(odd) { background-color: #fafaff; }
+    </style>
+</head>
+<body>
+<script>
+    var g_shownDataHashCodeHex = ""
+    var g_updTimer = null;
+
+    $(document).ready(function() {
+        $.getScript("js/testfails-2.0.js", function(data, textStatus, jqxhr){ });
+
+        $( document ).tooltip();
+        loadData();
+        //todo fix setInterval( function() { checkForUpdate(); }, 30000);
+
+        $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
+
+        if(g_updTimer==null) {
+            g_updTimer=setTimeout(tstTimeout, 3200);
+        }
+        setInterval(tstTimeout, 10000);
+    });
+
+    function tstTimeout() {
+
+        if(g_updTimer!=null) {
+            clearTimeout(g_updTimer);
+            g_updTimer=null;
+        }
+
+
+        var d = new Date();
+        var n = d.getTime();
+
+        // $(document.body).prepend("timeout at "+n+"<br>");
+
+
+        if(g_updTimer==null) {
+            g_updTimer=setTimeout(tstTimeout, 3200);
+        }
+    }
+
+    function parmsForRest() {
+        var curReqParms = "";
+        var server = findGetParameter("server");
+        if(server!=null) {
+            curReqParms += "&server=" + server;
+        }
+
+        var buildType = findGetParameter("buildType");
+        if(buildType!=null) {
+            curReqParms += "&buildType=" + buildType;
+        }
+
+        var branch = findGetParameter("branch");
+        if(branch!=null) {
+            curReqParms += "&branch=" + branch;
+        }
+
+        var count = findGetParameter("count");
+        if(count!=null) {
+            curReqParms += "&count=" + count;
+        }
+
+        curReqParms = curReqParms.replace("&","?");
+
+        return curReqParms;
+    }
+
+    function loadData() {
+        var curFailuresUrl = "rest/build/history" + parmsForRest();
+
+        $("#loadStatus").html("<img src='https://www.wallies.com/filebin/images/loading_apple.gif' width=20px height=20px> Please wait");
+        $.ajax({
+            url: curFailuresUrl,
+            success: function(result) {
+                if(result.updateRequired || (isDefinedAndFilled(result.runningUpdates) && result.runningUpdates>0)) {
+                    setTimeout(checkForUpdate, 3000)
+                    $("#loadStatus").html("<img src='https://www.wallies.com/filebin/images/loading_apple.gif' width=20px height=20px> Updating");
+                } else {
+                    $("#loadStatus").html("");
+                }
+                showData(result);
+                g_shownDataHashCodeHex = isDefinedAndFilled(result.hashCodeHex) ? result.hashCodeHex : "";
+            },
+            error: showErrInLoadStatus
+        });
+    }
+    function showData(result) {
+        $("#statistics").html(showBuildHistory(result));
+    }
+
+    function median(arr){
+        arr = arr.sort(function(a, b){ return a - b; });
+        var i = arr.length / 2;
+        return i % 1 == 0 ? (arr[i - 1] + arr[i]) / 2 : arr[Math.floor(i)];
+    }
+
+    function formatString(string) {
+        if (string.indexOf("_") < string.indexOf(":"))
+            string = string.substring(string.indexOf("_") + 1);
+
+        return "<b>" + string.substring(0, string.indexOf(":") + 1) + "</b>" + string.substring(string.indexOf(":") + 1);
+    }
+
+    function showBuildHistory(result) {
+        var res = "<table><tr><th>#</th><th>Id</th><th>Total tests</th><th>Failed tests</th><th>Ignored tests</th>" +
+            "<th>Muted tests</th><th>Total issues</th><th>Total run time</th></tr>";
+
+        var problemCountMin = result[0].problemsCount;
+        var problemCountMax = problemCountMin;
+        var problemCounts = [];
+        var average = 0;
+
+        for (var i = 0; i < result.length; i++) {
+            var buildStatistics = result[i];
+
+            problemCounts[i] = buildStatistics.problemsCount;
+
+            if (problemCounts[i] > problemCountMax) problemCountMax = problemCounts[i];
+
+            if (problemCounts[i] < problemCountMin) problemCountMin = problemCounts[i];
+
+            average += problemCounts[i] / buildStatistics.build.testOccurrences.failed;
+
+            res += "<tr><td>" + (i + 1) + "</td>" +
+                "<td><a href='" + buildStatistics.build.webUrl + "'>" + buildStatistics.build.id + "</a></td>" +
+                "<td>" + buildStatistics.build.testOccurrences.count + "</td>" +
+                "<td>" + buildStatistics.build.testOccurrences.failed + "</td>" +
+                "<td>" + buildStatistics.build.testOccurrences.ignored + "</td>" +
+                "<td>" + buildStatistics.build.testOccurrences.muted + "</td><td>";
+
+            if (buildStatistics.fullRes.length > 0) {
+                res += "<details><summary>" + formatString(buildStatistics.shortRes) + "</summary><p>";
+
+                for (var k = 0; k < buildStatistics.fullRes.length; k++) {
+                    res += (k + 1) + ". " + formatString(buildStatistics.fullRes[k]) + "<br>";
+                }
+
+                res += "</p></details><br>";
+            } else {
+                res += "<p>" + buildStatistics.shortRes + "</p>";
+            }
+
+            var issuesArr = buildStatistics.relatedIssues;
+
+            if (issuesArr.length > 0) {
+                res += "<details><summary>Related issues [" + issuesArr.length + "]</summary><p>";
+
+                for (var j = 0; j < issuesArr.length; j++) {
+                    var issue = issuesArr[j];
+
+                    if (j !== 0) res += ", ";
+
+                    res += "<a href='" + issue.url + "'>" + issue.id + "</a>";
+                }
+
+                res += "</p></details>";
+            }
+
+            res += "</td><td>" + buildStatistics.durationPrintable + "</td></tr>";
+        }
+
+        res += "</table><br>min: " + problemCountMin + "; max: " + problemCountMax +
+            "; median: " + (Math.round(median(problemCounts) * 1000) / 1000) +
+            "; average: " + (Math.round(average / result.length * 1000) / 1000) + "<br><br>";
+
+        return res;
+
+    }
+</script>
+<br>
+<div id="loadStatus"></div>
+<div id="statistics"></div>
+<div id="version"></div>
+</body>
+</html>
\ No newline at end of file


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services