Add dashboard app engine backend.

Change-Id: I168b5f24cb832ef8dffc82b62c688b034fb39be6
diff --git a/common/src/main/java/com/google/gwt/benchmark/common/shared/json/BenchmarkRunJson.java b/common/src/main/java/com/google/gwt/benchmark/common/shared/json/BenchmarkRunJson.java
index c01679a..95968eb 100644
--- a/common/src/main/java/com/google/gwt/benchmark/common/shared/json/BenchmarkRunJson.java
+++ b/common/src/main/java/com/google/gwt/benchmark/common/shared/json/BenchmarkRunJson.java
@@ -39,8 +39,6 @@
 
   void setCommitId(String commitId);
 
-  void setCommitTime(String commitTime);
-
   /**
    * Set the commit time of the patch in milliseconds since 1970.
    * <p>
diff --git a/dashboard/pom.xml b/dashboard/pom.xml
new file mode 100644
index 0000000..1fc29a1
--- /dev/null
+++ b/dashboard/pom.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.google.gwt.benchmark</groupId>
+  <artifactId>gwt-benchmark-dashboard</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>war</packaging>
+
+  <parent>
+    <groupId>com.google.gwt.benchmark</groupId>
+    <artifactId>gwt-benchmark-parent</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <gwtversion>2.6.1</gwtversion>
+    <appengine.target.version>1.9.1</appengine.target.version>
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>com.google.gwt.benchmark</groupId>
+      <artifactId>gwt-benchmark-common</artifactId>
+      <version>1.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-servlet</artifactId>
+      <version>3.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt.inject</groupId>
+      <artifactId>gin</artifactId>
+      <version>2.1.2</version>
+      <classifier />
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt.google-apis</groupId>
+      <artifactId>gwt-visualization</artifactId>
+      <version>1.0.2</version>
+    </dependency>
+
+
+    <!-- Compile/runtime dependencies -->
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-api-1.0-sdk</artifactId>
+      <version>${appengine.target.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.5</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>jstl</groupId>
+      <artifactId>jstl</artifactId>
+      <version>1.2</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>20090211</version>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.7</version>
+    </dependency>
+
+    <!-- Test Dependencies -->
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-testing</artifactId>
+      <version>${appengine.target.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-api-stubs</artifactId>
+      <version>${appengine.target.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-tools-sdk</artifactId>
+      <version>${appengine.target.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-api-labs</artifactId>
+      <version>${appengine.target.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-lang</groupId>
+      <artifactId>commons-lang</artifactId>
+      <version>2.3</version>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.jdo</groupId>
+      <artifactId>jdo-api</artifactId>
+      <version>3.0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.datanucleus</groupId>
+      <artifactId>datanucleus-core</artifactId>
+      <version>3.1.1</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.datanucleus</groupId>
+      <artifactId>datanucleus-api-jdo</artifactId>
+      <version>3.1.2</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.appengine.orm</groupId>
+      <artifactId>datanucleus-appengine</artifactId>
+      <version>2.1.2</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-user</artifactId>
+      <version>${gwtversion}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-servlet</artifactId>
+      <version>${gwtversion}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.web.bindery</groupId>
+      <artifactId>requestfactory-server</artifactId>
+      <version>${gwtversion}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>1.0.0.GA</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>1.0.0.GA</version>
+      <classifier>sources</classifier>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.persistence</groupId>
+      <artifactId>persistence-api</artifactId>
+      <version>1.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.4</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <version>1.9.5</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <version>2.5.1</version>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>2.3</version>
+        <configuration>
+          <!-- <archiveClasses>true</archiveClasses> -->
+          <webResources>
+            <!-- in order to interpolate version from pom into appengine-web.xml -->
+            <resource>
+              <directory>${basedir}/src/main/webapp/WEB-INF</directory>
+              <filtering>true</filtering>
+              <targetPath>WEB-INF</targetPath>
+            </resource>
+          </webResources>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>com.google.appengine</groupId>
+        <artifactId>appengine-maven-plugin</artifactId>
+        <version>${appengine.target.version}</version>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-eclipse-plugin</artifactId>
+        <version>2.8</version>
+
+        <configuration>
+          <downloadSources>true</downloadSources>
+          <downloadJavadocs>false</downloadJavadocs>
+          <buildOutputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</buildOutputDirectory>
+          <projectnatures>
+            <projectnature>org.eclipse.jdt.core.javanature</projectnature>
+            <projectnature>com.google.gdt.eclipse.core.webAppNature</projectnature>
+            <nature>com.google.appengine.eclipse.core.gaeNature</nature>
+            <nature>com.google.gwt.eclipse.core.gwtNature</nature>
+          </projectnatures>
+          <buildcommands>
+            <buildcommand>org.eclipse.jdt.core.javabuilder</buildcommand>
+            <buildcommand>com.google.gdt.eclipse.core.webAppProjectValidator</buildcommand>
+
+            <buildcommand>com.google.appengine.eclipse.core.projectValidator</buildcommand>
+            <buildcommand>com.google.gwt.eclipse.core.gwtProjectValidator</buildcommand>
+            <buildcommand>com.google.appengine.eclipse.core.enhancerbuilder</buildcommand>
+
+          </buildcommands>
+          <classpathContainers>
+            <classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer>
+            <classpathContainer>com.google.appengine.eclipse.core.GAE_CONTAINER</classpathContainer>
+            <classpathContainer>com.google.gwt.eclipse.core.GWT_CONTAINER</classpathContainer>
+          </classpathContainers>
+          <excludes>
+<!--             <exclude>com.google.gwt:gwt-servlet</exclude> -->
+            <exclude>com.google.gwt:gwt-user</exclude>
+            <exclude>com.google.gwt:gwt-dev</exclude>
+            <exclude>javax.validation:validation-api</exclude>
+            <exclude>com.google.appengine:appengine-api-1.0-sdk</exclude>
+            <exclude>org.datanucleus:datanucleus-core</exclude>
+            <exclude>org.datanucleus:datanucleus-enhancer</exclude>
+            <exclude>org.datanucleus:datanucleus-jpa</exclude>
+            <exclude>org.datanucleus:datanucleus-api-jdo</exclude>
+            <exclude>com.google.appengine.orm:datanucleus-appengine</exclude>
+          </excludes>
+
+
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.datanucleus</groupId>
+        <artifactId>maven-datanucleus-plugin</artifactId>
+        <version>3.1.2</version>
+        <configuration>
+          <log4jConfiguration>${basedir}/log4j.properties</log4jConfiguration>
+          <verbose>false</verbose>
+          <fork>false</fork>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>process-classes</phase>
+            <goals>
+              <goal>enhance</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+</project>
+
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/AuthController.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/AuthController.java
new file mode 100644
index 0000000..5e77394
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/AuthController.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+import com.google.appengine.api.datastore.DatastoreFailureException;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.KeyFactory;
+
+import java.util.ConcurrentModificationException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AuthContoller stores and verifies an auth token.
+ */
+public class AuthController {
+
+  private static final Logger logger = Logger.getLogger(AuthController.class.getName());
+
+  public void updateAuth(String auth) throws ControllerException {
+
+    if (auth == null || auth.length() < 6) {
+      throw new ControllerException("Auth empty or too short (min. 6 chars)");
+    }
+
+    try {
+      DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+      Entity entity = new Entity(KeyFactory.createKey("Auth", "auth"));
+      entity.setProperty("value", auth);
+      datastore.put(entity);
+    } catch (ConcurrentModificationException | DatastoreFailureException e) {
+      logger.log(Level.WARNING, "Can not persist new auth", e);
+      throw new ControllerException("Can not persist new auth", e);
+    }
+  }
+
+  public boolean validateAuth(String auth) {
+    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+    try {
+      Entity entity = datastore.get(KeyFactory.createKey("Auth", "auth"));
+      String authFromStore = (String)entity.getProperty("value");
+      if(authFromStore.equals(auth)) {
+        return true;
+      }
+      logger.severe("Auth failed validation");
+      return false;
+    } catch (EntityNotFoundException e) {
+      logger.log(Level.SEVERE, "No auth entry in datastore", e);
+      return false;
+    }
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkController.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkController.java
new file mode 100644
index 0000000..938f32a
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkController.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+import com.google.appengine.api.datastore.DatastoreFailureException;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.DatastoreTimeoutException;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.taskqueue.Queue;
+import com.google.appengine.api.taskqueue.QueueFactory;
+import com.google.appengine.api.taskqueue.TaskOptions;
+import com.google.gwt.benchmark.common.shared.json.BenchmarkResultJson;
+import com.google.gwt.benchmark.common.shared.json.BenchmarkRunJson;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkGraph;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkResult;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkRun;
+import com.google.gwt.benchmark.dashboard.server.guice.DashboardServletGuiceModule;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This controller is responsible for adding and retrieving benchmark data.
+ */
+public class BenchmarkController {
+
+  private static final Logger logger = Logger.getLogger(BenchmarkController.class.getName());
+
+  private static class ToPersist {
+
+    private final long commitTimeMsEpoch;
+    private final BenchmarkRun benchmarkRun;
+    private final List<BenchmarkResult> benchmarkResults;
+
+    public ToPersist(BenchmarkRun benchmarkRun, List<BenchmarkResult> benchmarkResults,
+        long commitTimeMsEpoch) {
+      this.benchmarkRun = benchmarkRun;
+      this.benchmarkResults = Collections.unmodifiableList(benchmarkResults);
+      this.commitTimeMsEpoch = commitTimeMsEpoch;
+    }
+  }
+
+  private static class WeekSpan {
+    private final int commitWeek;
+    private final int commitYear;
+    private final long weekStartMsEpoch;
+    private final long weekEndMsEpoch;
+
+    public WeekSpan(int commitWeek, int commitYear, long weekStartMsEpoch, long weekEndMsEpoch) {
+      this.commitWeek = commitWeek;
+      this.commitYear = commitYear;
+      this.weekStartMsEpoch = weekStartMsEpoch;
+      this.weekEndMsEpoch = weekEndMsEpoch;
+    }
+  }
+
+  private static class BenchmarkGraphData {
+    private final List<String> commitIds;
+    private final List<Double> runsPerSecond;
+
+    public BenchmarkGraphData(List<String> commitIds, List<Double> runsPerSecond) {
+      this.commitIds = Collections.unmodifiableList(commitIds);
+      this.runsPerSecond = Collections.unmodifiableList(runsPerSecond);
+    }
+  }
+
+  public void addBenchmarkResult(BenchmarkRunJson benchmarkRunJSON) throws ControllerException {
+    ToPersist toPersist = createDomainObjects(benchmarkRunJSON);
+    persistBenchmarkRun(toPersist, 3);
+    addUpdateRequestToTaskQueue(toPersist);
+  }
+
+  public void updateGraph(long commitTimeMsEpoch, String benchmarkName, String runnerId)
+      throws ControllerException {
+
+    WeekSpan weekSpan = createWeekSpan(commitTimeMsEpoch);
+
+    BenchmarkGraphData benchmarkGraphData = calculateNewGraphData(benchmarkName, runnerId,
+        weekSpan.weekStartMsEpoch, weekSpan.weekEndMsEpoch);
+
+    putBenchmarkGraph(benchmarkName, runnerId, weekSpan.commitWeek, weekSpan.commitYear,
+        benchmarkGraphData.commitIds, benchmarkGraphData.runsPerSecond);
+  }
+
+  private BenchmarkGraphData calculateNewGraphData(String benchmarkName, String runnerId,
+      long weekStartMsEpoch, long weekEndMsEpoch) {
+
+    DatastoreService dataStore = DatastoreServiceFactory.getDatastoreService();
+    Query query =
+        new Query(BenchmarkRun.NAME).addSort("commitTimeMsEpoch", SortDirection.ASCENDING);
+    Filter startFilter = new Query.FilterPredicate("commitTimeMsEpoch",
+        FilterOperator.GREATER_THAN_OR_EQUAL, weekStartMsEpoch);
+    Filter endFilter =
+        new Query.FilterPredicate("commitTimeMsEpoch", FilterOperator.LESS_THAN, weekEndMsEpoch);
+
+    Filter compositeFilter = Query.CompositeFilterOperator.and(startFilter, endFilter);
+    query.setFilter(compositeFilter);
+    PreparedQuery prepare = dataStore.prepare(query);
+
+    List<Entity> entityList = prepare.asList(FetchOptions.Builder.withDefaults());
+
+    ArrayList<Key> keys = new ArrayList<Key>(entityList.size());
+    List<String> commitIds = new ArrayList<>(entityList.size());
+    for (Entity entity : entityList) {
+      keys.add(BenchmarkResult.createKey(entity.getKey(), benchmarkName, runnerId));
+      commitIds.add(new BenchmarkRun(entity).getCommitId());
+    }
+
+    Map<Key, Entity> brMap = dataStore.get(keys);
+    List<BenchmarkResult> results = new ArrayList<>(brMap.size());
+
+    for (Key key : keys) {
+      results.add(new BenchmarkResult(brMap.get(key)));
+    }
+
+    List<Double> runsPerSecond = new ArrayList<>(entityList.size());
+    for (BenchmarkResult benchmarkResult : results) {
+      runsPerSecond.add(benchmarkResult.getRunsPerMinute());
+    }
+
+    return new BenchmarkGraphData(commitIds, runsPerSecond);
+  }
+
+  private void putBenchmarkGraph(String benchmarkName, String runnerId, int week, int year,
+      List<String> commitIds, List<Double> runsPerSecond) throws ControllerException {
+    BenchmarkGraph graph = new BenchmarkGraph(benchmarkName, runnerId, week, year);
+    graph.setRunsPerSecond(runsPerSecond);
+    graph.setCommitIds(commitIds);
+    try {
+      DatastoreService dataStore = DatastoreServiceFactory.getDatastoreService();
+      dataStore.put(graph.getEntity());
+    } catch (DatastoreFailureException | ConcurrentModificationException e) {
+      throw new ControllerException("Can not persist BenchmarkGraph", e);
+    }
+  }
+
+  private WeekSpan createWeekSpan(long commitTimeMsEpoch) {
+    Calendar cal = Calendar.getInstance();
+    cal.setTime(new Date(commitTimeMsEpoch));
+    int week = cal.get(Calendar.WEEK_OF_YEAR);
+    int year = cal.get(Calendar.YEAR);
+
+    Calendar calendar = Calendar.getInstance();
+    calendar.clear();
+    calendar.set(Calendar.WEEK_OF_YEAR, week);
+    calendar.set(Calendar.YEAR, year);
+    long startSearch = calendar.getTimeInMillis();
+
+    calendar.add(Calendar.DAY_OF_YEAR, 7);
+    long endSearch = calendar.getTimeInMillis();
+
+    return new WeekSpan(week, year, startSearch, endSearch);
+  }
+
+  private void addUpdateRequestToTaskQueue(ToPersist toPersist) {
+    Queue queue = QueueFactory.getQueue("graph-queue");
+    for (BenchmarkResult benchmarkResult : toPersist.benchmarkResults) {
+      TaskOptions taskOptions = TaskOptions.Builder.withUrl(
+          DashboardServletGuiceModule.GRAPH_QUEUE_URL).param("commitTimeMsEpoch",
+          String.valueOf(toPersist.commitTimeMsEpoch)).param("benchmarkName",
+          benchmarkResult.getBenchmarkName()).param("runnerId", benchmarkResult.getRunnerId());
+      queue.add(taskOptions);
+    }
+  }
+
+  private ToPersist createDomainObjects(BenchmarkRunJson benchmarkRunJSON) {
+    String commitId = benchmarkRunJSON.getCommitId();
+    long commitTimeMsEpoch = Math.round(benchmarkRunJSON.getCommitTimeMsEpoch());
+    BenchmarkRun benchmarkRun = new BenchmarkRun(commitId, commitTimeMsEpoch);
+    List<BenchmarkResult> brToPersist = new ArrayList<>();
+
+    HashSet<String> runnerIds = new HashSet<>();
+    Map<String, List<BenchmarkResultJson>> results = benchmarkRunJSON.getResultByBenchmarkName();
+
+    for (Entry<String, List<BenchmarkResultJson>> entry : results.entrySet()) {
+
+      String moduleName = entry.getKey();
+      List<BenchmarkResultJson> listResults = entry.getValue();
+      for (BenchmarkResultJson benchmarkResultJSON : listResults) {
+        runnerIds.add(benchmarkResultJSON.getRunnerId());
+        BenchmarkResult benchmarkResult = new BenchmarkResult(benchmarkRun.getKey(), moduleName,
+            benchmarkResultJSON.getRunnerId());
+        benchmarkResult.setRunsPerMinute(benchmarkResultJSON.getRunsPerMinute());
+        brToPersist.add(benchmarkResult);
+      }
+    }
+
+    benchmarkRun.setRunnerIds(new ArrayList<>(runnerIds));
+
+    return new ToPersist(benchmarkRun, brToPersist, commitTimeMsEpoch);
+  }
+
+  private void persistBenchmarkRun(ToPersist toPersist, int retryCount) throws ControllerException {
+    for(int i = 0; i < retryCount; i++) {
+        logger.info(String.format("persistBenchmarkRun try %d", (i+1)));
+        if (persistBenchmarkRun(toPersist)) {
+          logger.info(String.format("persistBenchmarkRun try %d succeded", (i+1)));
+          return;
+        }
+        logger.info(String.format("persistBenchmarkRun try %d failed", (i+1)));
+    }
+
+    logger.warning((String.format("persistBenchmarkRun gave up after %d retries", retryCount)));
+    throw new ControllerException(
+        (String.format("persistBenchmarkRun gave up after %d retries", retryCount)));
+  }
+
+  private boolean persistBenchmarkRun(ToPersist toPersist) {
+    // If a client will try to persist the same benchmark run twice, we will simply
+    // update the existing entry in the datastore, effectively meaning last entry wins.
+    // There is an uncovered edge case: If a benchmark run is reuploaded with fewer executed
+    // benchmarks these entries will be left in the datastore.
+
+    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+    Transaction transaction = null;
+
+    try {
+      transaction = datastore.beginTransaction();
+      List<Entity> entities = new ArrayList<>();
+      for (BenchmarkResult br : toPersist.benchmarkResults) {
+        entities.add(br.getEntity());
+      }
+      List<Key> list = datastore.put(entities);
+      BenchmarkRun benchmarkRun = toPersist.benchmarkRun;
+      benchmarkRun.setResults(list);
+      datastore.put(benchmarkRun.getEntity());
+      transaction.commit();
+      return true;
+    } catch (DatastoreTimeoutException | DatastoreFailureException
+        | ConcurrentModificationException e) {
+      logger.log(Level.WARNING, "Can not persist benchmark results", e);
+      return false;
+    } finally {
+      if (transaction != null && transaction.isActive()) {
+        transaction.rollback();
+      }
+    }
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkProblem.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkProblem.java
new file mode 100644
index 0000000..baabcd9
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkProblem.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+/**
+ * A benchmark problem indicates a regression in a benchmark.
+ */
+public class BenchmarkProblem {
+  private String module;
+
+  private String text;
+
+  private String runnerId;
+
+  public BenchmarkProblem(String module, String runnerId, String text) {
+    this.module = module;
+    this.runnerId = runnerId;
+    this.text = text;
+  }
+
+  public String getModule() {
+    return module;
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public String getRunnerId() {
+    return runnerId;
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/ControllerException.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/ControllerException.java
new file mode 100644
index 0000000..5232860
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/controller/ControllerException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+/**
+ * Generic base exception for all controllers.
+ */
+public class ControllerException extends Exception {
+
+  public ControllerException(String message) {
+    super(message);
+  }
+
+  public ControllerException(String message, Throwable e) {
+    super(message, e);
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkGraph.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkGraph.java
new file mode 100644
index 0000000..a3bbab1
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkGraph.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.domain;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A BencharmGraph contains all results for a certain module and runner for one week.
+ */
+public class BenchmarkGraph {
+
+  public static final String NAME = "Graph";
+
+  public static Key createKey(String benchmarkName, String runnerId, int week, int year) {
+    return KeyFactory.createKey(NAME,
+        BenchmarkGraph.createName(benchmarkName, runnerId, week, year));
+  }
+
+  private static String createName(String module, String runnerId, int week, int year) {
+    return module + "_" + runnerId + "_" + week + "_" + year;
+  }
+
+  private Entity entity;
+
+  /**
+   * Creates a new, empty graph.
+   */
+  public BenchmarkGraph(String module, String runnerId, int week, int year) {
+    entity = new Entity(createKey(module, runnerId, week, year));
+    setModule(module);
+    setYear(year);
+    setWeek(week);
+    setCommitIds(new ArrayList<String>());
+    setRunsPerSecond(new ArrayList<Double>());
+    setRunnerId(runnerId);
+  }
+
+  /**
+   * Wraps an existing graph loaded from the datastore.
+   */
+  public BenchmarkGraph(Entity entity) {
+    this.entity = entity;
+  }
+
+  private void setWeek(int week) {
+    entity.setProperty("week", week);
+  }
+
+  public int getWeek() {
+    return ((Long) entity.getProperty("week")).intValue();
+  }
+
+  private void setYear(int year) {
+    entity.setProperty("year", year);
+  }
+
+  public int getYear() {
+    return ((Long) entity.getProperty("year")).intValue();
+  }
+
+  @SuppressWarnings("unchecked")
+  public List<String> getCommitIds() {
+    return Collections.unmodifiableList((List<String>) entity.getProperty("commitIds"));
+  }
+
+  public void setCommitIds(List<String> commitIds) {
+    entity.setProperty("commitIds", commitIds);
+  }
+
+  private void setModule(String module) {
+    entity.setProperty("module", module);
+  }
+
+  public String getModule() {
+    return (String) entity.getProperty("module");
+  }
+
+  public String getRunnerId() {
+    return (String) entity.getProperty("runnerId");
+  }
+
+  private void setRunnerId(String runnerId) {
+    entity.setProperty("runnerId", runnerId);
+  }
+
+  public void setRunsPerSecond(List<Double> runsPerSecond) {
+    entity.setProperty("runsPerSecond", runsPerSecond);
+  }
+
+  @SuppressWarnings("unchecked")
+  public List<Double> getRunsPerSecond() {
+    return Collections.unmodifiableList((List<Double>) entity.getProperty("runsPerSecond"));
+  }
+
+  public Entity getEntity() {
+    return entity;
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkResult.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkResult.java
new file mode 100644
index 0000000..1b205b2
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkResult.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.domain;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+
+/**
+ * A BenchmarkResult contains the runs per minute for one module on one runner.
+ */
+public class BenchmarkResult {
+
+  public static Key createKey(Key run, String benchmarkName, String runnerId) {
+    String name = createName(benchmarkName, runnerId);
+    return KeyFactory.createKey(run, NAME, name);
+  }
+
+  private static String createName(String benchmarkName, String runnerId) {
+    return benchmarkName + "&" + runnerId;
+  }
+
+  public static final String NAME = "BenchmarkResult";
+
+  private Entity entity;
+
+  public BenchmarkResult(Key run, String benchmarkName, String runnerId) {
+    entity = new Entity(createKey(run, benchmarkName, runnerId));
+    entity.setProperty("runnerId", runnerId);
+    entity.setProperty("benchmarkName", benchmarkName);
+    setRunsPerMinute(0);
+  }
+
+  public BenchmarkResult(Entity entity) {
+    this.entity = entity;
+  }
+
+  public String getBenchmarkName() {
+    return (String) entity.getProperty("benchmarkName");
+  }
+
+  public String getRunnerId() {
+    return (String) entity.getProperty("runnerId");
+  }
+
+  public void setRunsPerMinute(double runsPerMinute) {
+    entity.setProperty("runsPerMinute", runsPerMinute);
+  }
+
+  public double getRunsPerMinute() {
+    return (double) entity.getProperty("runsPerMinute");
+  }
+
+  public Key getKey() {
+    return entity.getKey();
+  }
+
+  public Entity getEntity() {
+    return entity;
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkRun.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkRun.java
new file mode 100644
index 0000000..8590332
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/domain/BenchmarkRun.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.domain;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A BenchmarkRun contains all information for one execution of all benchmarks.
+ */
+public class BenchmarkRun {
+
+  public static final String NAME = "BenchmarkRun";
+
+  public static Key createKey(String commitId) {
+    return KeyFactory.createKey(NAME, commitId);
+  }
+
+  private Entity entity;
+
+  public BenchmarkRun(String commitId, long commitTimeMsEpoch) {
+    entity = new Entity(createKey(commitId));
+    entity.setProperty("commitId", commitId);
+    entity.setProperty("commitTimeMsEpoch", commitTimeMsEpoch);
+  }
+
+  public BenchmarkRun(Entity entity) {
+    this.entity = entity;
+  }
+
+  public void setRunnerIds(List<String> runnerIds) {
+    entity.setProperty("runnerIds", runnerIds);
+  }
+
+  @SuppressWarnings("unchecked")
+  public List<String> getRunnerIds() {
+    return Collections.unmodifiableList((List<String>) entity.getProperty("runnerIds"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public List<Key> getResults() {
+    return Collections.unmodifiableList((List<Key>) entity.getProperty("results"));
+  }
+
+  public void setResults(List<Key> results) {
+    entity.setProperty("results", results);
+  }
+
+  public String getCommitId() {
+    return (String) entity.getProperty("commitId");
+  }
+
+  public long getCommitTimeMsEpoch() {
+    return (long) entity.getProperty("commitTimeMsEpoch");
+  }
+
+  public Key getKey() {
+    return entity.getKey();
+  }
+
+  public Entity getEntity() {
+    return entity;
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/DashboardServletGuiceModule.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/DashboardServletGuiceModule.java
new file mode 100644
index 0000000..9e8625e
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/DashboardServletGuiceModule.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.guice;
+
+import com.google.gwt.benchmark.dashboard.server.servlets.AddBenchmarkResultServlet;
+import com.google.gwt.benchmark.dashboard.server.servlets.AuthServlet;
+import com.google.gwt.benchmark.dashboard.server.servlets.GraphUpdateWorkerServlet;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+/**
+ * Guice module for the Dashboard.
+ */
+public class DashboardServletGuiceModule extends ServletModule {
+
+  public static final String GRAPH_QUEUE_URL = "/tasks/graph-queue";
+
+  @Override
+  protected void configureServlets() {
+    bind(AddBenchmarkResultServlet.class).in(Singleton.class);
+    serve("/post_result").with(AddBenchmarkResultServlet.class);
+    bind(AuthServlet.class).in(Singleton.class);
+    serve("/admin/").with(AuthServlet.class);
+    bind(GraphUpdateWorkerServlet.class).in(Singleton.class);
+    serve(GRAPH_QUEUE_URL).with(GraphUpdateWorkerServlet.class);
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/GuiceServletConfig.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/GuiceServletConfig.java
new file mode 100644
index 0000000..6bd2efa
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/guice/GuiceServletConfig.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.guice;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.servlet.GuiceServletContextListener;
+
+/**
+ * Servlet context listener, will be invoked by the servlet container.
+ */
+public class GuiceServletConfig extends GuiceServletContextListener {
+
+  @Override
+  protected Injector getInjector() {
+    return Guice.createInjector(new DashboardServletGuiceModule());
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServlet.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServlet.java
new file mode 100644
index 0000000..082f5a5
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServlet.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.servlets;
+
+import com.google.gwt.benchmark.common.shared.json.BenchmarkRunJson;
+import com.google.gwt.benchmark.common.shared.json.JsonFactory;
+import com.google.gwt.benchmark.dashboard.server.controller.AuthController;
+import com.google.gwt.benchmark.dashboard.server.controller.BenchmarkController;
+import com.google.web.bindery.autobean.shared.AutoBean;
+import com.google.web.bindery.autobean.shared.AutoBeanCodex;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A put request on this servlet will add a new benchmark to the dashboard.
+ */
+public class AddBenchmarkResultServlet extends HttpServlet {
+
+  private static final Logger logger = Logger.getLogger(AddBenchmarkResultServlet.class.getName());
+
+  private final BenchmarkController benchmarkController;
+  private final AuthController authController;
+
+  @Inject
+  public AddBenchmarkResultServlet(AuthController authController, BenchmarkController benchmarkController) {
+    this.authController = authController;
+    this.benchmarkController = benchmarkController;
+  }
+
+  @Override
+  protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
+      IOException {
+
+    String auth = req.getHeader("auth");
+    if (!authController.validateAuth(auth)) {
+      resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+      return;
+    }
+
+    BenchmarkRunJson benchmarkRunJSON = null;
+    String json = null;
+    try {
+      json = IOUtils.toString(req.getInputStream(), "UTF-8");
+      AutoBean<BenchmarkRunJson> bean =
+          AutoBeanCodex.decode(JsonFactory.get(), BenchmarkRunJson.class, json);
+      benchmarkRunJSON = bean.as();
+    } catch (Exception e) {
+      logger.log(Level.WARNING, "Can not deserialize JSON", e);
+      if (json != null) {
+        logger.warning(json);
+      }
+      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      resp.getWriter().write("Can't parse JSON, see App Engine log for details.");
+      return;
+    }
+
+    try {
+
+      benchmarkController.addBenchmarkResult(benchmarkRunJSON);
+      resp.setStatus(HttpServletResponse.SC_OK);
+    } catch (Exception e) {
+      logger.log(Level.WARNING, "Can not add benchmark results", e);
+      resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AuthServlet.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AuthServlet.java
new file mode 100644
index 0000000..4ac6978
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/AuthServlet.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.servlets;
+
+import com.google.gwt.benchmark.dashboard.server.controller.AuthController;
+import com.google.gwt.benchmark.dashboard.server.controller.ControllerException;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This servlet can update the auth phrase required to post benchmarks.
+ */
+public class AuthServlet extends HttpServlet {
+
+  private final AuthController authController;
+
+  @Inject
+  public AuthServlet(AuthController authController) {
+    this.authController = authController;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    request.getRequestDispatcher("/admin/change_pwd_form.html").forward(request, response);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
+      IOException {
+    String newPassword = req.getParameter("password");
+    try {
+      authController.updateAuth(newPassword);
+    } catch (ControllerException e) {
+      resp.getWriter().print(e.getMessage());
+    }
+  }
+}
diff --git a/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/GraphUpdateWorkerServlet.java b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/GraphUpdateWorkerServlet.java
new file mode 100644
index 0000000..220d2cb
--- /dev/null
+++ b/dashboard/src/main/java/com/google/gwt/benchmark/dashboard/server/servlets/GraphUpdateWorkerServlet.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.servlets;
+
+import com.google.gwt.benchmark.dashboard.server.controller.BenchmarkController;
+import com.google.gwt.benchmark.dashboard.server.controller.ControllerException;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This servlet handles request for the graph update queue.
+ */
+public class GraphUpdateWorkerServlet extends HttpServlet {
+
+  private static final Logger logger = Logger.getLogger(GraphUpdateWorkerServlet.class.getName());
+
+  private BenchmarkController controller;
+
+
+  @Inject
+  public GraphUpdateWorkerServlet(BenchmarkController controller) {
+    this.controller = controller;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
+      IOException {
+
+    long commitTimeMsEpoch = Long.parseLong(request.getParameter("commitTimeMsEpoch"));
+    String benchmarkName = request.getParameter("benchmarkName");
+    String runnerId = request.getParameter("runnerId");
+
+    logger.info(String.format("Received update request for graph (%s %s %d", benchmarkName, runnerId, commitTimeMsEpoch));
+
+    try {
+      controller.updateGraph(commitTimeMsEpoch, benchmarkName, runnerId);
+    } catch (ControllerException e) {
+      logger.log(Level.WARNING, "Can not update graph", e);
+      throw new ServletException("Can not update graph", e);
+    }
+  }
+}
diff --git a/dashboard/src/main/webapp/WEB-INF/appengine-web.xml b/dashboard/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 0000000..a6fe8e3
--- /dev/null
+++ b/dashboard/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
+    <application>gwt-bench</application>
+    <version>1</version>
+    <threadsafe>true</threadsafe>
+
+    <system-properties>
+        <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+    </system-properties>
+</appengine-web-app>
diff --git a/dashboard/src/main/webapp/WEB-INF/logging.properties b/dashboard/src/main/webapp/WEB-INF/logging.properties
new file mode 100644
index 0000000..0c2ea51
--- /dev/null
+++ b/dashboard/src/main/webapp/WEB-INF/logging.properties
@@ -0,0 +1,13 @@
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+#
+# <system-properties>
+#   <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+# </system-properties>
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/dashboard/src/main/webapp/WEB-INF/queue.xml b/dashboard/src/main/webapp/WEB-INF/queue.xml
new file mode 100644
index 0000000..a1762be
--- /dev/null
+++ b/dashboard/src/main/webapp/WEB-INF/queue.xml
@@ -0,0 +1,7 @@
+<queue-entries>
+  <queue>
+    <name>graph-queue</name>
+    <rate>5/s</rate>
+    <bucket-size>40</bucket-size>
+  </queue>
+</queue-entries>
\ No newline at end of file
diff --git a/dashboard/src/main/webapp/WEB-INF/web.xml b/dashboard/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..54cc5f3
--- /dev/null
+++ b/dashboard/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
+
+  <welcome-file-list>
+    <welcome-file>index.html</welcome-file>
+  </welcome-file-list>
+
+  <filter>
+    <filter-name>guiceFilter</filter-name>
+    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
+  </filter>
+
+  <filter-mapping>
+    <filter-name>guiceFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+  </filter-mapping>
+
+  <listener>
+    <listener-class>com.google.gwt.benchmark.dashboard.server.guice.GuiceServletConfig</listener-class>
+  </listener>
+
+  <security-constraint>
+    <web-resource-collection>
+      <web-resource-name>admin</web-resource-name>
+        <url-pattern>/admin/*</url-pattern>
+      </web-resource-collection>
+      <auth-constraint>
+        <role-name>admin</role-name>
+      </auth-constraint>
+  </security-constraint>
+
+  <security-constraint>
+    <web-resource-collection>
+      <web-resource-name>post_result</web-resource-name>
+      <url-pattern>/post_result</url-pattern>
+    </web-resource-collection>
+    <user-data-constraint>
+      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+    </user-data-constraint>
+  </security-constraint>
+
+  <security-constraint>
+    <web-resource-collection>
+      <web-resource-name>tasks</web-resource-name>
+        <url-pattern>/tasks/*</url-pattern>
+    </web-resource-collection>
+    <auth-constraint>
+        <role-name>admin</role-name>
+    </auth-constraint>
+  </security-constraint>
+
+</web-app>
diff --git a/dashboard/src/main/webapp/admin/change_pwd_form.html b/dashboard/src/main/webapp/admin/change_pwd_form.html
new file mode 100644
index 0000000..22312d3
--- /dev/null
+++ b/dashboard/src/main/webapp/admin/change_pwd_form.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+  <head></head>
+  <body>
+    <form action="/admin/" method="post">
+      <div>
+        <label for="password">New Password:</label>
+        <input type="password" name="password" />
+      </div>
+    </form>
+  </body>
+</html>
\ No newline at end of file
diff --git a/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/AuthControllerTest.java b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/AuthControllerTest.java
new file mode 100644
index 0000000..1a50aeb
--- /dev/null
+++ b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/AuthControllerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for {@link AuthController}.
+ */
+public class AuthControllerTest {
+
+  private final LocalServiceTestHelper helper =
+      new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+
+  @Before
+  public void setUp() {
+    helper.setUp();
+  }
+
+  @After
+  public void tearDown() {
+    helper.tearDown();
+  }
+
+  @Test
+  public void testValidValue() throws ControllerException {
+    // empty datastore should always return false
+    Assert.assertFalse(new AuthController().validateAuth("someauth1"));
+
+    // prepare datastore
+    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+    Entity entity = new Entity(KeyFactory.createKey("Auth", "auth"));
+    entity.setProperty("value", "someauth2");
+    datastore.put(entity);
+
+    // now see if auth validation works
+    Assert.assertTrue(new AuthController().validateAuth("someauth2"));
+    Assert.assertFalse(new AuthController().validateAuth("invalidAuth"));
+
+    // change the auth
+    new AuthController().updateAuth("changed auth");
+
+    // does the change propagate?
+    Assert.assertTrue(new AuthController().validateAuth("changed auth"));
+    Assert.assertFalse(new AuthController().validateAuth("invalidAuth1"));
+
+    // test for two short auths
+    try {
+      new AuthController().updateAuth("12345");
+      Assert.fail("exception did not occur (auth too short)");
+    } catch (ControllerException ignored) {
+    }
+
+    // verify that it did not change
+    Assert.assertTrue(new AuthController().validateAuth("changed auth"));
+  }
+
+}
diff --git a/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkControllerTest.java b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkControllerTest.java
new file mode 100644
index 0000000..c248bbd
--- /dev/null
+++ b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/controller/BenchmarkControllerTest.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.controller;
+
+import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.taskqueue.dev.LocalTaskQueue;
+import com.google.appengine.api.taskqueue.dev.QueueStateInfo;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
+import com.google.gwt.benchmark.common.shared.json.BenchmarkRunJson;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkGraph;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkResult;
+import com.google.gwt.benchmark.dashboard.server.domain.BenchmarkRun;
+import com.google.gwt.benchmark.dashboard.server.servlets.AddBenchmarkResultServletTest;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test for {@link BenchmarkController}.
+ */
+public class BenchmarkControllerTest {
+
+  private final LocalServiceTestHelper helper = new LocalServiceTestHelper(
+      new LocalDatastoreServiceTestConfig(), new LocalTaskQueueTestConfig().setQueueXmlPath(
+          System.getProperty("user.dir") + "/src/test/resources/queue.xml"));
+
+  @Before
+  public void setUp() {
+    helper.setUp();
+  }
+
+  @After
+  public void tearDown() {
+    helper.tearDown();
+  }
+
+  @Test
+  public void testControllerPersists() throws ControllerException, EntityNotFoundException {
+
+    BenchmarkRunJson benchmarkRunJSON = AddBenchmarkResultServletTest.buildBenchmarkRunJSON();
+
+    BenchmarkController controller = new BenchmarkController();
+
+    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+
+    Assert.assertEquals(0, ds.prepare(new Query(BenchmarkRun.NAME)).countEntities(withLimit(10)));
+    Assert.assertEquals(0, ds.prepare(new Query(BenchmarkGraph.NAME)).countEntities(withLimit(10)));
+    Assert.assertEquals(0,
+        ds.prepare(new Query(BenchmarkResult.NAME)).countEntities(withLimit(10)));
+    controller.addBenchmarkResult(benchmarkRunJSON);
+
+    Assert.assertEquals(1, ds.prepare(new Query(BenchmarkRun.NAME)).countEntities(withLimit(10)));
+
+    String commitId = benchmarkRunJSON.getCommitId();
+
+    long runtime = Math.round(benchmarkRunJSON.getCommitTimeMsEpoch());
+
+    Key benchmarkRunKey = BenchmarkRun.createKey(commitId);
+    Key module1FireFox = BenchmarkResult.createKey(benchmarkRunKey, "module1", "firefox_linux");
+    Key module1Chrome = BenchmarkResult.createKey(benchmarkRunKey, "module1", "chrome_linux");
+    Key module2FireFox = BenchmarkResult.createKey(benchmarkRunKey, "module2", "firefox_linux");
+    Key module2Chrome = BenchmarkResult.createKey(benchmarkRunKey, "module2", "chrome_linux");
+
+    Entity entity = ds.get(benchmarkRunKey);
+
+    BenchmarkRun benchmarkRun = new BenchmarkRun(entity);
+
+    Assert.assertEquals(commitId, benchmarkRun.getCommitId());
+    Assert.assertEquals(runtime, benchmarkRun.getCommitTimeMsEpoch());
+
+    Assert.assertEquals(2, benchmarkRun.getRunnerIds().size());
+    Assert.assertTrue(benchmarkRun.getRunnerIds().contains("firefox_linux"));
+    Assert.assertTrue(benchmarkRun.getRunnerIds().contains("chrome_linux"));
+
+    Assert.assertEquals(4, benchmarkRun.getResults().size());
+    Assert.assertTrue(benchmarkRun.getResults().contains(module1FireFox));
+    Assert.assertTrue(benchmarkRun.getResults().contains(module1Chrome));
+    Assert.assertTrue(benchmarkRun.getResults().contains(module2FireFox));
+    Assert.assertTrue(benchmarkRun.getResults().contains(module2Chrome));
+
+    entity = ds.get(module1FireFox);
+    BenchmarkResult benchmarkResult = new BenchmarkResult(entity);
+    Assert.assertEquals("module1", benchmarkResult.getBenchmarkName());
+    Assert.assertEquals("firefox_linux", benchmarkResult.getRunnerId());
+    Assert.assertEquals(3, benchmarkResult.getRunsPerMinute(), 0.0001);
+
+    entity = ds.get(module1Chrome);
+    benchmarkResult = new BenchmarkResult(entity);
+    Assert.assertEquals("module1", benchmarkResult.getBenchmarkName());
+    Assert.assertEquals("chrome_linux", benchmarkResult.getRunnerId());
+    Assert.assertEquals(4, benchmarkResult.getRunsPerMinute(), 0.0001);
+
+    entity = ds.get(module2FireFox);
+    benchmarkResult = new BenchmarkResult(entity);
+    Assert.assertEquals("module2", benchmarkResult.getBenchmarkName());
+    Assert.assertEquals("firefox_linux", benchmarkResult.getRunnerId());
+    Assert.assertEquals(3, benchmarkResult.getRunsPerMinute(), 0.0001);
+
+    entity = ds.get(module2Chrome);
+    benchmarkResult = new BenchmarkResult(entity);
+    Assert.assertEquals("module2", benchmarkResult.getBenchmarkName());
+    Assert.assertEquals("chrome_linux", benchmarkResult.getRunnerId());
+    Assert.assertEquals(4, benchmarkResult.getRunsPerMinute(), 0.0001);
+
+    LocalTaskQueue taskQueue = LocalTaskQueueTestConfig.getLocalTaskQueue();
+
+    Map<String, QueueStateInfo> queueStateInfo = taskQueue.getQueueStateInfo();
+    QueueStateInfo info = queueStateInfo.get("graph-queue");
+    Assert.assertEquals(4, info.getCountTasks());
+  }
+
+  @Test
+  public void testGraphUpdate() throws ControllerException {
+    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+    long commitTime_20th_march_2014 = 1400610555950L;
+    long oneWeekInMs = 604800000L;
+
+    List<String> runnerIds = Arrays.asList("linux_ff", "linux_chrome");
+
+    // put in one entity for the current week
+    BenchmarkRun benchmarkRun = new BenchmarkRun("commitId0", commitTime_20th_march_2014);
+    benchmarkRun.setRunnerIds(runnerIds);
+    BenchmarkResult benchmarkResult1 =
+        new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_ff");
+    benchmarkResult1.setRunsPerMinute(100);
+    ds.put(benchmarkResult1.getEntity());
+    BenchmarkResult benchmarkResult2 =
+        new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_chrome");
+    benchmarkResult2.setRunsPerMinute(200);
+    ds.put(benchmarkResult2.getEntity());
+    BenchmarkResult benchmarkResult3 =
+        new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_ff");
+    benchmarkResult3.setRunsPerMinute(300);
+    ds.put(benchmarkResult3.getEntity());
+    BenchmarkResult benchmarkResult4 =
+        new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_chrome");
+    benchmarkResult4.setRunsPerMinute(400);
+    ds.put(benchmarkResult4.getEntity());
+
+    benchmarkRun.setResults(Arrays.asList(benchmarkResult1.getKey(), benchmarkResult2.getKey(),
+        benchmarkResult3.getKey(), benchmarkResult4.getKey()));
+
+    ds.put(benchmarkRun.getEntity());
+
+    // put in another entity for the current week (a little bit later)
+    benchmarkRun = new BenchmarkRun("commitId1", commitTime_20th_march_2014 + 5000L);
+    benchmarkRun.setRunnerIds(runnerIds);
+    benchmarkResult1 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_ff");
+    benchmarkResult1.setRunsPerMinute(101);
+    ds.put(benchmarkResult1.getEntity());
+    benchmarkResult2 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_chrome");
+    benchmarkResult2.setRunsPerMinute(201);
+    ds.put(benchmarkResult2.getEntity());
+    benchmarkResult3 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_ff");
+    benchmarkResult3.setRunsPerMinute(301);
+    ds.put(benchmarkResult3.getEntity());
+    benchmarkResult4 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_chrome");
+    benchmarkResult4.setRunsPerMinute(401);
+    ds.put(benchmarkResult4.getEntity());
+
+    benchmarkRun.setResults(Arrays.asList(benchmarkResult1.getKey(), benchmarkResult2.getKey(),
+        benchmarkResult3.getKey(), benchmarkResult4.getKey()));
+
+    ds.put(benchmarkRun.getEntity());
+
+    // put in one entity the week before
+    benchmarkRun = new BenchmarkRun("commitId2", commitTime_20th_march_2014 - oneWeekInMs);
+    benchmarkRun.setRunnerIds(runnerIds);
+    benchmarkResult1 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_ff");
+    benchmarkResult1.setRunsPerMinute(102);
+    ds.put(benchmarkResult1.getEntity());
+    benchmarkResult2 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_chrome");
+    benchmarkResult2.setRunsPerMinute(202);
+    ds.put(benchmarkResult2.getEntity());
+    benchmarkResult3 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_ff");
+    benchmarkResult3.setRunsPerMinute(302);
+    ds.put(benchmarkResult3.getEntity());
+    benchmarkResult4 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_chrome");
+    benchmarkResult4.setRunsPerMinute(402);
+    ds.put(benchmarkResult4.getEntity());
+
+    benchmarkRun.setResults(Arrays.asList(benchmarkResult1.getKey(), benchmarkResult2.getKey(),
+        benchmarkResult3.getKey(), benchmarkResult4.getKey()));
+
+    ds.put(benchmarkRun.getEntity());
+
+    // put in one entity the week after
+    benchmarkRun = new BenchmarkRun("commitId3", commitTime_20th_march_2014 + oneWeekInMs);
+    benchmarkRun.setRunnerIds(runnerIds);
+    benchmarkResult1 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_ff");
+    benchmarkResult1.setRunsPerMinute(103);
+    ds.put(benchmarkResult1.getEntity());
+    benchmarkResult2 = new BenchmarkResult(benchmarkRun.getKey(), "module1", "linux_chrome");
+    benchmarkResult2.setRunsPerMinute(203);
+    ds.put(benchmarkResult2.getEntity());
+    benchmarkResult3 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_ff");
+    benchmarkResult3.setRunsPerMinute(303);
+    ds.put(benchmarkResult3.getEntity());
+    benchmarkResult4 = new BenchmarkResult(benchmarkRun.getKey(), "module2", "linux_chrome");
+    benchmarkResult4.setRunsPerMinute(403);
+    ds.put(benchmarkResult4.getEntity());
+
+    benchmarkRun.setResults(Arrays.asList(benchmarkResult1.getKey(), benchmarkResult2.getKey(),
+        benchmarkResult3.getKey(), benchmarkResult4.getKey()));
+
+    ds.put(benchmarkRun.getEntity());
+
+    BenchmarkController controller = new BenchmarkController();
+
+    // update the first graph
+    controller.updateGraph(commitTime_20th_march_2014, "module1", "linux_ff");
+    // get an verify
+    Query query = new Query(BenchmarkGraph.NAME);
+    List<Entity> list = ds.prepare(query).asList(FetchOptions.Builder.withDefaults());
+    Assert.assertEquals(1, list.size());
+
+    BenchmarkGraph benchmarkGraph = new BenchmarkGraph(list.get(0));
+    Assert.assertEquals("module1", benchmarkGraph.getModule());
+    Assert.assertEquals("linux_ff", benchmarkGraph.getRunnerId());
+    Assert.assertEquals(2, benchmarkGraph.getCommitIds().size());
+    Assert.assertEquals("commitId0", benchmarkGraph.getCommitIds().get(0));
+    Assert.assertEquals("commitId1", benchmarkGraph.getCommitIds().get(1));
+    Assert.assertEquals(21, benchmarkGraph.getWeek());
+    Assert.assertEquals(2014, benchmarkGraph.getYear());
+    Assert.assertEquals(2, benchmarkGraph.getRunsPerSecond().size());
+    Assert.assertEquals(100, benchmarkGraph.getRunsPerSecond().get(0), 0.0001);
+    Assert.assertEquals(101, benchmarkGraph.getRunsPerSecond().get(1), 0.0001);
+    // delete the entity
+    ds.delete(benchmarkGraph.getEntity().getKey());
+
+    // update the second graph
+    controller.updateGraph(commitTime_20th_march_2014, "module2", "linux_ff");
+    // get an verify
+    query = new Query(BenchmarkGraph.NAME);
+    list = ds.prepare(query).asList(FetchOptions.Builder.withDefaults());
+    Assert.assertEquals(1, list.size());
+
+    benchmarkGraph = new BenchmarkGraph(list.get(0));
+    Assert.assertEquals("module2", benchmarkGraph.getModule());
+    Assert.assertEquals("linux_ff", benchmarkGraph.getRunnerId());
+    Assert.assertEquals(2, benchmarkGraph.getCommitIds().size());
+    Assert.assertEquals("commitId0", benchmarkGraph.getCommitIds().get(0));
+    Assert.assertEquals("commitId1", benchmarkGraph.getCommitIds().get(1));
+    Assert.assertEquals(21, benchmarkGraph.getWeek());
+    Assert.assertEquals(2014, benchmarkGraph.getYear());
+    Assert.assertEquals(2, benchmarkGraph.getRunsPerSecond().size());
+    Assert.assertEquals(300, benchmarkGraph.getRunsPerSecond().get(0), 0.0001);
+    Assert.assertEquals(301, benchmarkGraph.getRunsPerSecond().get(1), 0.0001);
+    // delete the entity
+    ds.delete(benchmarkGraph.getEntity().getKey());
+
+    // update the third graph
+    controller.updateGraph(commitTime_20th_march_2014, "module1", "linux_chrome");
+    // get an verify
+    query = new Query(BenchmarkGraph.NAME);
+    list = ds.prepare(query).asList(FetchOptions.Builder.withDefaults());
+    Assert.assertEquals(1, list.size());
+
+    benchmarkGraph = new BenchmarkGraph(list.get(0));
+    Assert.assertEquals("module1", benchmarkGraph.getModule());
+    Assert.assertEquals("linux_chrome", benchmarkGraph.getRunnerId());
+    Assert.assertEquals(2, benchmarkGraph.getCommitIds().size());
+    Assert.assertEquals("commitId0", benchmarkGraph.getCommitIds().get(0));
+    Assert.assertEquals("commitId1", benchmarkGraph.getCommitIds().get(1));
+    Assert.assertEquals(21, benchmarkGraph.getWeek());
+    Assert.assertEquals(2014, benchmarkGraph.getYear());
+    Assert.assertEquals(2, benchmarkGraph.getRunsPerSecond().size());
+    Assert.assertEquals(200, benchmarkGraph.getRunsPerSecond().get(0), 0.0001);
+    Assert.assertEquals(201, benchmarkGraph.getRunsPerSecond().get(1), 0.0001);
+    // delete the entity
+    ds.delete(benchmarkGraph.getEntity().getKey());
+
+    // update the third graph
+    controller.updateGraph(commitTime_20th_march_2014, "module2", "linux_chrome");
+    // get an verify
+    query = new Query(BenchmarkGraph.NAME);
+    list = ds.prepare(query).asList(FetchOptions.Builder.withDefaults());
+    Assert.assertEquals(1, list.size());
+
+    benchmarkGraph = new BenchmarkGraph(list.get(0));
+    Assert.assertEquals("module2", benchmarkGraph.getModule());
+    Assert.assertEquals("linux_chrome", benchmarkGraph.getRunnerId());
+    Assert.assertEquals(2, benchmarkGraph.getCommitIds().size());
+    Assert.assertEquals("commitId0", benchmarkGraph.getCommitIds().get(0));
+    Assert.assertEquals("commitId1", benchmarkGraph.getCommitIds().get(1));
+    Assert.assertEquals(21, benchmarkGraph.getWeek());
+    Assert.assertEquals(2014, benchmarkGraph.getYear());
+    Assert.assertEquals(2, benchmarkGraph.getRunsPerSecond().size());
+    Assert.assertEquals(400, benchmarkGraph.getRunsPerSecond().get(0), 0.0001);
+    Assert.assertEquals(401, benchmarkGraph.getRunsPerSecond().get(1), 0.0001);
+    // delete the entity
+    ds.delete(benchmarkGraph.getEntity().getKey());
+  }
+}
diff --git a/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServletTest.java b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServletTest.java
new file mode 100644
index 0000000..be24c90
--- /dev/null
+++ b/dashboard/src/test/java/com/google/gwt/benchmark/dashboard/server/servlets/AddBenchmarkResultServletTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.benchmark.dashboard.server.servlets;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gwt.benchmark.common.shared.json.BenchmarkResultJson;
+import com.google.gwt.benchmark.common.shared.json.BenchmarkRunJson;
+import com.google.gwt.benchmark.common.shared.json.JsonFactory;
+import com.google.gwt.benchmark.dashboard.server.controller.AuthController;
+import com.google.gwt.benchmark.dashboard.server.controller.BenchmarkController;
+import com.google.gwt.benchmark.dashboard.server.controller.ControllerException;
+import com.google.gwt.benchmark.dashboard.server.servlets.AddBenchmarkResultServlet;
+import com.google.web.bindery.autobean.shared.AutoBean;
+import com.google.web.bindery.autobean.shared.AutoBeanCodex;
+import com.google.web.bindery.autobean.shared.AutoBeanUtils;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Test for {@link AddBenchmarkResultServlet}.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AddBenchmarkResultServletTest {
+
+  public static BenchmarkRunJson buildBenchmarkRunJSON() {
+    JsonFactory.Factory factory = JsonFactory.get();
+
+    BenchmarkRunJson runJSON = factory.run().as();
+    runJSON.setCommitId("commit1");
+    runJSON.setCommitTimeMsEpoch(1);
+
+    Map<String, List<BenchmarkResultJson>> results = new LinkedHashMap<>();
+    String moduleName = "module1";
+    List<BenchmarkResultJson> list = new ArrayList<>();
+    results.put(moduleName, list);
+
+    BenchmarkResultJson resultJSON = factory.result().as();
+    resultJSON.setBenchmarkName(moduleName);
+    resultJSON.setRunnerId("firefox_linux");
+    resultJSON.setRunsPerMinute(3);
+    list.add(resultJSON);
+
+    BenchmarkResultJson resultJSON1 = factory.result().as();
+    resultJSON1.setBenchmarkName(moduleName);
+    resultJSON1.setRunnerId("chrome_linux");
+    resultJSON1.setRunsPerMinute(4);
+    list.add(resultJSON1);
+
+    String moduleName1 = "module2";
+    List<BenchmarkResultJson> list1 = new ArrayList<>();
+    results.put(moduleName1, list1);
+
+    BenchmarkResultJson resultJSON2 = factory.result().as();
+    resultJSON2.setBenchmarkName(moduleName1);
+    resultJSON2.setRunnerId("firefox_linux");
+    resultJSON2.setRunsPerMinute(3);
+    list1.add(resultJSON2);
+    BenchmarkResultJson resultJSON3 = factory.result().as();
+    resultJSON3.setBenchmarkName(moduleName1);
+    resultJSON3.setRunnerId("chrome_linux");
+    resultJSON3.setRunsPerMinute(4);
+    list1.add(resultJSON3);
+
+    runJSON.setResultByBenchmarkName(results);
+    return runJSON;
+  }
+
+  private AddBenchmarkResultServlet servlet;
+
+  private AutoBean<BenchmarkRunJson> bean;
+  @Mock
+  private BenchmarkController benchmarkController;
+  @Mock
+  private HttpServletResponse response;
+  @Mock
+  private HttpServletRequest request;
+
+  @Mock
+  private AuthController authController;
+
+  @Before
+  public void setup() throws IOException {
+
+    bean = AutoBeanUtils.getAutoBean(buildBenchmarkRunJSON());
+
+    final ByteArrayInputStream byteArrayInputStream =
+        new ByteArrayInputStream(AutoBeanCodex.encode(bean).getPayload().getBytes("UTF-8"));
+    ServletInputStream servletInputStream = new ServletInputStream() {
+
+      @Override
+      public int read() throws IOException {
+        return byteArrayInputStream.read();
+      }
+    };
+    when(request.getInputStream()).thenReturn(servletInputStream);
+
+    servlet = new AddBenchmarkResultServlet(authController, benchmarkController);
+  }
+
+  @Test
+  public void testSuccessfulResultAdd() throws ServletException, IOException, ControllerException {
+    when(request.getHeader("auth")).thenReturn("authtest");
+    when(authController.validateAuth("authtest")).thenReturn(true);
+
+    servlet.doPut(request, response);
+    ArgumentCaptor<BenchmarkRunJson> captor = ArgumentCaptor.forClass(BenchmarkRunJson.class);
+    verify(benchmarkController).addBenchmarkResult(captor.capture());
+    BenchmarkRunJson value = captor.getValue();
+    AutoBean<BenchmarkRunJson> autoBean = AutoBeanUtils.getAutoBean(value);
+    Assert.assertTrue(AutoBeanUtils.deepEquals(bean, autoBean));
+    verify(response).setStatus(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void testBadAuth() throws ServletException, IOException {
+    when(request.getHeader("auth")).thenReturn("wrongauth");
+    when(authController.validateAuth("wrongauth")).thenReturn(false);
+    servlet.doPut(request, response);
+    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  @Test
+  public void testBadController() throws ServletException, IOException, ControllerException {
+    when(request.getHeader("auth")).thenReturn("authtest");
+    when(authController.validateAuth("authtest")).thenReturn(true);
+    doThrow(new ControllerException("test", null)).when(benchmarkController).addBenchmarkResult(
+        Mockito.<BenchmarkRunJson> anyObject());
+    servlet.doPut(request, response);
+    verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+}
diff --git a/dashboard/src/test/resources/queue.xml b/dashboard/src/test/resources/queue.xml
new file mode 100644
index 0000000..a1762be
--- /dev/null
+++ b/dashboard/src/test/resources/queue.xml
@@ -0,0 +1,7 @@
+<queue-entries>
+  <queue>
+    <name>graph-queue</name>
+    <rate>5/s</rate>
+    <bucket-size>40</bucket-size>
+  </queue>
+</queue-entries>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 958d956..2d27081 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,5 +15,6 @@
         <module>common</module>
         <module>compileserver</module>
         <module>launcher</module>
+        <module>dashboard</module>
     </modules>
 </project>