Perf-o-matic: generate dashboard images for 30, 90, and 365 days
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 1 Mar 2012 21:55:49 +0000 (21:55 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 1 Mar 2012 21:55:49 +0000 (21:55 +0000)
https://bugs.webkit.org/show_bug.cgi?id=80016

Reviewed by Eric Seidel.

Add the ability to generate graphs for 30, 90, and 365 day graphs but only occasionally
according to the heuristics in DashboardImage.needs_update.

Also fix a bug in Runs.chart_params that y-axis grid was set to an incorrect value.

* Websites/webkit-perf.appspot.com/controller.py:
(schedule_runs_update):
(RunsChartHandler.post):
* Websites/webkit-perf.appspot.com/json_generators.py:
(Runs.chart_params):
* Websites/webkit-perf.appspot.com/json_generators_unittest.py:
(RunsTest.test_chart_params_with_value):
* Websites/webkit-perf.appspot.com/models.py:
(DashboardImage):
(DashboardImage.needs_update):
* Websites/webkit-perf.appspot.com/models_unittest.py:
(HelperTests._assert_there_is_exactly_one_id_holder_and_matches):
(HelperTests.test_create_in_transaction_with_numeric_id_holder.execute):
(HelperTests.test_create_in_transaction_with_numeric_id_holder):
(HelperTests.test_failing_in_create_in_transaction_with_numeric_id_holder):
(HelperTests.test_raising_in_create_in_transaction_with_numeric_id_holder):
(HelperTests.test_delete_model_with_numeric_id_holder.execute):
(HelperTests.test_delete_model_with_numeric_id_holder):
(HelperTests.test_model_from_numeric_id.execute):
(HelperTests):
(HelperTests.test_model_from_numeric_id):
(BranchTests.test_create_if_possible):
(PlatformTests.test_create_if_possible):
(BuilderTests.test_create):
(BuilderTests.test_update_password):
(BuilderTests.test_hashed_password):
(BuilderTests.test_authenticate):
(_create_some_builder):
(_create_build):
(BuildTests.test_get_or_insert_from_log):
(TestModelTests.test_update_or_insert):
(TestModelTests.test_update_or_insert_to_update):
(TestModelTests.test_merge):
(TestResultTests.test_get_or_insert_value):
(TestResultTests.test_get_or_insert_stat_value):
(TestResultTests.test_replace_to_change_test_name):
(TestResultTests.test_replace_to_change_test_name_with_stat_value):
(TestResultTests.test_replace_to_change_test_name_overrides_conflicting_result):
(ReportLogTests._create_log_with_payload):
(ReportLogTests.test_builder):
(ReportLogTests.test_branch):
(ReportLogTests.test_platform):
(PersistentCacheTests._assert_persistent_cache):
(PersistentCacheTests.test_set):
(PersistentCacheTests.test_get):
(DashboardImageTests):
(DashboardImageTests.test_needs_update):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@109418 268f45cc-cd09-0410-ab3c-d52691b4dbfc

ChangeLog
Websites/webkit-perf.appspot.com/controller.py
Websites/webkit-perf.appspot.com/json_generators.py
Websites/webkit-perf.appspot.com/json_generators_unittest.py
Websites/webkit-perf.appspot.com/models.py
Websites/webkit-perf.appspot.com/models_unittest.py

index 40e29b4..52283de 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,63 @@
+2012-03-01  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Perf-o-matic: generate dashboard images for 30, 90, and 365 days
+        https://bugs.webkit.org/show_bug.cgi?id=80016
+
+        Reviewed by Eric Seidel.
+
+        Add the ability to generate graphs for 30, 90, and 365 day graphs but only occasionally
+        according to the heuristics in DashboardImage.needs_update.
+
+        Also fix a bug in Runs.chart_params that y-axis grid was set to an incorrect value.
+        * Websites/webkit-perf.appspot.com/controller.py:
+        (schedule_runs_update):
+        (RunsChartHandler.post):
+        * Websites/webkit-perf.appspot.com/json_generators.py:
+        (Runs.chart_params):
+        * Websites/webkit-perf.appspot.com/json_generators_unittest.py:
+        (RunsTest.test_chart_params_with_value):
+        * Websites/webkit-perf.appspot.com/models.py:
+        (DashboardImage):
+        (DashboardImage.needs_update):
+        * Websites/webkit-perf.appspot.com/models_unittest.py:
+        (HelperTests._assert_there_is_exactly_one_id_holder_and_matches):
+        (HelperTests.test_create_in_transaction_with_numeric_id_holder.execute):
+        (HelperTests.test_create_in_transaction_with_numeric_id_holder):
+        (HelperTests.test_failing_in_create_in_transaction_with_numeric_id_holder):
+        (HelperTests.test_raising_in_create_in_transaction_with_numeric_id_holder):
+        (HelperTests.test_delete_model_with_numeric_id_holder.execute):
+        (HelperTests.test_delete_model_with_numeric_id_holder):
+        (HelperTests.test_model_from_numeric_id.execute):
+        (HelperTests):
+        (HelperTests.test_model_from_numeric_id):
+        (BranchTests.test_create_if_possible):
+        (PlatformTests.test_create_if_possible):
+        (BuilderTests.test_create):
+        (BuilderTests.test_update_password):
+        (BuilderTests.test_hashed_password):
+        (BuilderTests.test_authenticate):
+        (_create_some_builder):
+        (_create_build):
+        (BuildTests.test_get_or_insert_from_log):
+        (TestModelTests.test_update_or_insert):
+        (TestModelTests.test_update_or_insert_to_update):
+        (TestModelTests.test_merge):
+        (TestResultTests.test_get_or_insert_value):
+        (TestResultTests.test_get_or_insert_stat_value):
+        (TestResultTests.test_replace_to_change_test_name):
+        (TestResultTests.test_replace_to_change_test_name_with_stat_value):
+        (TestResultTests.test_replace_to_change_test_name_overrides_conflicting_result):
+        (ReportLogTests._create_log_with_payload):
+        (ReportLogTests.test_builder):
+        (ReportLogTests.test_branch):
+        (ReportLogTests.test_platform):
+        (PersistentCacheTests._assert_persistent_cache):
+        (PersistentCacheTests.test_set):
+        (PersistentCacheTests.test_get):
+        (DashboardImageTests):
+        (DashboardImageTests.test_needs_update):
+
 2012-03-01  Kangil Han  <kangil.han@samsung.com>
 
         [DRT] Remove all PlainTextController usages in existing tests by adding internal API
index 0c0d61a..1e03579 100644 (file)
@@ -99,8 +99,10 @@ def cache_runs(test_id, branch_id, platform_id, cache):
 
 def schedule_runs_update(test_id, branch_id, platform_id):
     taskqueue.add(url='/api/test/runs/update', params={'id': test_id, 'branchid': branch_id, 'platformid': platform_id})
-    taskqueue.add(url='/api/test/runs/chart', params={'id': test_id, 'branchid': branch_id, 'platformid': platform_id,
-        'displayDays': 7})
+    for display_days in [7, 30, 90, 365]:
+        if DashboardImage.needs_update(branch_id, test_id, platform_id, display_days):
+            taskqueue.add(url='/api/test/runs/chart', params={'id': test_id, 'branchid': branch_id, 'platformid': platform_id,
+                'displayDays': display_days})
 
 
 def _get_test_branch_platform_ids(handler):
@@ -167,8 +169,6 @@ class RunsChartHandler(webapp2.RequestHandler):
         DashboardImage(key_name=DashboardImage.key_name(branch.id, platform.id, test.id, display_days),
             image=dashboard_chart_file.read()).put()
 
-        self.response.out.write('Fetched http://chart.googleapis.com/chart?%s' % urllib.urlencode(params))
-
 
 class DashboardImageHandler(webapp2.RequestHandler):
     def get(self, test_id, branch_id, platform_id, display_days):
index b2dca9b..4e8b196 100644 (file)
@@ -182,7 +182,7 @@ class Runs(JSONGeneratorBase):
             'date_range': None,  # Never used by common.js.
             'stat': 'ok'}
 
-    def chart_params(self, display_days, now=datetime.now()):
+    def chart_params(self, display_days, now=datetime.now().replace(hour=12, minute=0, second=0, microsecond=0)):
         chart_data_x = []
         chart_data_y = []
         end_time = now
@@ -196,11 +196,10 @@ class Runs(JSONGeneratorBase):
             chart_data_x.append(timestamp)
             chart_data_y.append(result.value)
 
-        dates = [end_time + timedelta(day - display_days) for day in range(0, display_days + 1)]
+        dates = [end_time - timedelta(display_days / 7.0 * (7 - i)) for i in range(0, 8)]
 
         y_max = max(chart_data_y) * 1.1
-        y_grid_step = y_max / 5
-        y_axis_label_step = int(y_grid_step + 0.5)  # This won't work for decimal numbers
+        y_axis_label_step = int(y_max / 5 + 0.5)  # This won't work for decimal numbers
 
         return {
             'cht': 'lxy',  # Specify with X and Y coordinates
@@ -211,7 +210,7 @@ class Runs(JSONGeneratorBase):
             'chxs': '1,676767,11.167,0,l,676767',  # Y-axis label: 1,color,font-size,centerd on tick,axis line/no ticks, tick color
             'chs': '360x240',  # Image size: 360px by 240px
             'chco': 'ff0000',  # Plot line color
-            'chg': '%f,%f,0,0' % (100 / (len(dates) - 1), y_grid_step),  # X, Y grid line step sizes - max for X is 100.
+            'chg': '%f,20,0,0' % (100 / (len(dates) - 1)),  # X, Y grid line step sizes - max is 100.
             'chls': '3',  # Line thickness
             'chf': 'bg,s,eff6fd',  # Transparent background
             'chd': 't:' + ','.join([str(x) for x in chart_data_x]) + '|' + ','.join([str(y) for y in chart_data_y]),  # X, Y data
index f9af990..3c819bf 100644 (file)
@@ -330,8 +330,17 @@ class RunsTest(DataStoreTestsBase):
         self.assertEqual(datetime.fromtimestamp(x_max), end_time)
         self.assertEqual(y_min, 0)
         self.assertEqual(y_max, int(52 * 1.1))
-        self.assertEqual(split_as_int(params['chg']), [int(100 / 7), int(52 * 1.1 / 5), 0, 0])
+        self.assertEqual(split_as_int(params['chg']), [int(100 / 7), 20, 0, 0])
 
+        params = Runs(some_branch, some_platform, 'some-test').chart_params(14, end_time)
+        self.assertEqual(params['chxl'], '0:|Feb 14|Feb 16|Feb 18|Feb 20|Feb 22|Feb 24|Feb 26|Feb 28')
+        self.assertEqual(split_as_int(params['chxr']), [1, 0, 57, int(52 * 1.1 / 5 + 0.5)])
+        x_min, x_max, y_min, y_max = split_as_int(params['chds'])
+        self.assertEqual(datetime.fromtimestamp(x_min), datetime(2011, 2, 14, 12, 0, 0))
+        self.assertEqual(datetime.fromtimestamp(x_max), end_time)
+        self.assertEqual(y_min, 0)
+        self.assertEqual(y_max, int(52 * 1.1))
+        self.assertEqual(split_as_int(params['chg']), [int(100 / 7), 20, 0, 0])
 
 
 if __name__ == '__main__':
index 6a54e50..f692419 100644 (file)
 
 import hashlib
 import json
+import math
 import re
 
 from datetime import datetime
+from datetime import timedelta
 from google.appengine.ext import db
 from google.appengine.api import memcache
 from time import mktime
@@ -326,6 +328,16 @@ class PersistentCache(db.Model):
 
 class DashboardImage(db.Model):
     image = db.BlobProperty(required=True)
+    createdAt = db.DateTimeProperty(required=True, auto_now=True)
+
+    @classmethod
+    def needs_update(cls, branch_id, platform_id, test_id, display_days, now=datetime.now()):
+        if display_days < 10:
+            return True
+        image = DashboardImage.get_by_key_name(cls.key_name(branch_id, platform_id, test_id, display_days))
+        duration = math.sqrt(display_days) / 10
+        # e.g. 13 hours for 30 days, 23 hours for 90 days, and 46 hours for 365 days
+        return not image or image.createdAt < now - timedelta(duration)
 
     @staticmethod
     def key_name(branch_id, platform_id, test_id, display_days):
index 4838f13..1f4c1bf 100644 (file)
@@ -31,10 +31,25 @@ import models
 import unittest
 
 from datetime import datetime
+from datetime import timedelta
 from google.appengine.api import memcache
 from google.appengine.ext import testbed
 from time import mktime
 
+from models import NumericIdHolder
+from models import Branch
+from models import Platform
+from models import Builder
+from models import Build
+from models import Test
+from models import TestResult
+from models import ReportLog
+from models import PersistentCache
+from models import DashboardImage
+from models import create_in_transaction_with_numeric_id_holder
+from models import delete_model_with_numeric_id_holder
+from models import model_from_numeric_id
+
 
 class DataStoreTestsBase(unittest.TestCase):
     def setUp(self):
@@ -65,7 +80,7 @@ class DataStoreTestsBase(unittest.TestCase):
 
 class HelperTests(DataStoreTestsBase):
     def _assert_there_is_exactly_one_id_holder_and_matches(self, id):
-        id_holders = models.NumericIdHolder.all().fetch(5)
+        id_holders = NumericIdHolder.all().fetch(5)
         self.assertEqual(len(id_holders), 1)
         self.assertTrue(id_holders[0])
         self.assertEqual(id_holders[0].key().id(), id)
@@ -73,14 +88,14 @@ class HelperTests(DataStoreTestsBase):
     def test_create_in_transaction_with_numeric_id_holder(self):
 
         def execute(id):
-            return models.Branch(id=id, name='some branch', key_name='some-branch').put()
+            return Branch(id=id, name='some branch', key_name='some-branch').put()
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
-        self.assertTrue(models.create_in_transaction_with_numeric_id_holder(execute))
+        self.assertTrue(create_in_transaction_with_numeric_id_holder(execute))
 
-        branches = models.Branch.all().fetch(5)
+        branches = Branch.all().fetch(5)
         self.assertEqual(len(branches), 1)
         self.assertEqual(branches[0].name, 'some branch')
         self.assertEqual(branches[0].key().name(), 'some-branch')
@@ -92,13 +107,13 @@ class HelperTests(DataStoreTestsBase):
         def execute(id):
             return None
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
-        self.assertFalse(models.create_in_transaction_with_numeric_id_holder(execute))
+        self.assertFalse(create_in_transaction_with_numeric_id_holder(execute))
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
     def test_raising_in_create_in_transaction_with_numeric_id_holder(self):
 
@@ -106,111 +121,111 @@ class HelperTests(DataStoreTestsBase):
             raise TypeError
             return None
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
-        self.assertRaises(TypeError, models.create_in_transaction_with_numeric_id_holder, (execute))
+        self.assertRaises(TypeError, create_in_transaction_with_numeric_id_holder, (execute))
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
     def test_delete_model_with_numeric_id_holder(self):
 
         def execute(id):
-            return models.Branch(id=id, name='some branch', key_name='some-branch').put()
+            return Branch(id=id, name='some branch', key_name='some-branch').put()
 
-        branch = models.Branch.get(models.create_in_transaction_with_numeric_id_holder(execute))
+        branch = Branch.get(create_in_transaction_with_numeric_id_holder(execute))
         self.assertOnlyInstance(branch)
 
-        models.delete_model_with_numeric_id_holder(branch)
+        delete_model_with_numeric_id_holder(branch)
 
-        self.assertThereIsNoInstanceOf(models.Branch)
-        self.assertThereIsNoInstanceOf(models.NumericIdHolder)
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertThereIsNoInstanceOf(NumericIdHolder)
 
     def test_model_from_numeric_id(self):
 
         def execute(id):
-            return models.Branch(id=id, name='some branch', key_name='some-branch').put()
+            return Branch(id=id, name='some branch', key_name='some-branch').put()
 
-        branch = models.Branch.get(models.create_in_transaction_with_numeric_id_holder(execute))
+        branch = Branch.get(create_in_transaction_with_numeric_id_holder(execute))
 
-        self.assertEqual(models.model_from_numeric_id(branch.id, models.Branch).key(), branch.key())
-        self.assertEqual(models.model_from_numeric_id(branch.id + 1, models.Branch), None)
-        models.delete_model_with_numeric_id_holder(branch)
-        self.assertEqual(models.model_from_numeric_id(branch.id, models.Branch), None)
+        self.assertEqual(model_from_numeric_id(branch.id, Branch).key(), branch.key())
+        self.assertEqual(model_from_numeric_id(branch.id + 1, Branch), None)
+        delete_model_with_numeric_id_holder(branch)
+        self.assertEqual(model_from_numeric_id(branch.id, Branch), None)
 
 
 class BranchTests(DataStoreTestsBase):
     def test_create_if_possible(self):
-        self.assertThereIsNoInstanceOf(models.Branch)
+        self.assertThereIsNoInstanceOf(Branch)
 
-        branch = models.Branch.create_if_possible('some-branch', 'some branch')
+        branch = Branch.create_if_possible('some-branch', 'some branch')
         self.assertTrue(branch)
         self.assertTrue(branch.key().name(), 'some-branch')
         self.assertTrue(branch.name, 'some branch')
         self.assertOnlyInstance(branch)
 
-        self.assertFalse(models.Branch.create_if_possible('some-branch', 'some other branch'))
+        self.assertFalse(Branch.create_if_possible('some-branch', 'some other branch'))
         self.assertTrue(branch.name, 'some branch')
         self.assertOnlyInstance(branch)
 
 
 class PlatformTests(DataStoreTestsBase):
     def test_create_if_possible(self):
-        self.assertThereIsNoInstanceOf(models.Platform)
+        self.assertThereIsNoInstanceOf(Platform)
 
-        platform = models.Platform.create_if_possible('some-platform', 'some platform')
+        platform = Platform.create_if_possible('some-platform', 'some platform')
         self.assertTrue(platform)
         self.assertTrue(platform.key().name(), 'some-platform')
         self.assertTrue(platform.name, 'some platform')
         self.assertOnlyInstance(platform)
 
-        self.assertFalse(models.Platform.create_if_possible('some-platform', 'some other platform'))
+        self.assertFalse(Platform.create_if_possible('some-platform', 'some other platform'))
         self.assertTrue(platform.name, 'some platform')
         self.assertOnlyInstance(platform)
 
 
 class BuilderTests(DataStoreTestsBase):
     def test_create(self):
-        builder_key = models.Builder.create('some builder', 'some password')
+        builder_key = Builder.create('some builder', 'some password')
         self.assertTrue(builder_key)
-        builder = models.Builder.get(builder_key)
+        builder = Builder.get(builder_key)
         self.assertEqual(builder.key().name(), 'some builder')
         self.assertEqual(builder.name, 'some builder')
-        self.assertEqual(builder.password, models.Builder._hashed_password('some password'))
+        self.assertEqual(builder.password, Builder._hashed_password('some password'))
 
     def test_update_password(self):
-        builder = models.Builder.get(models.Builder.create('some builder', 'some password'))
-        self.assertEqual(builder.password, models.Builder._hashed_password('some password'))
+        builder = Builder.get(Builder.create('some builder', 'some password'))
+        self.assertEqual(builder.password, Builder._hashed_password('some password'))
         builder.update_password('other password')
-        self.assertEqual(builder.password, models.Builder._hashed_password('other password'))
+        self.assertEqual(builder.password, Builder._hashed_password('other password'))
 
         # Make sure it's saved
-        builder = models.Builder.get(builder.key())
-        self.assertEqual(builder.password, models.Builder._hashed_password('other password'))
+        builder = Builder.get(builder.key())
+        self.assertEqual(builder.password, Builder._hashed_password('other password'))
 
     def test_hashed_password(self):
-        self.assertNotEqual(models.Builder._hashed_password('some password'), 'some password')
-        self.assertFalse('some password' in models.Builder._hashed_password('some password'))
-        self.assertEqual(len(models.Builder._hashed_password('some password')), 64)
+        self.assertNotEqual(Builder._hashed_password('some password'), 'some password')
+        self.assertFalse('some password' in Builder._hashed_password('some password'))
+        self.assertEqual(len(Builder._hashed_password('some password')), 64)
 
     def test_authenticate(self):
-        builder = models.Builder.get(models.Builder.create('some builder', 'some password'))
+        builder = Builder.get(Builder.create('some builder', 'some password'))
         self.assertTrue(builder.authenticate('some password'))
         self.assertFalse(builder.authenticate('bad password'))
 
 
 def _create_some_builder():
-    branch = models.Branch.create_if_possible('some-branch', 'Some Branch')
-    platform = models.Platform.create_if_possible('some-platform', 'Some Platform')
-    builder_key = models.Builder.create('some-builder', 'Some Builder')
-    return branch, platform, models.Builder.get(builder_key)
+    branch = Branch.create_if_possible('some-branch', 'Some Branch')
+    platform = Platform.create_if_possible('some-platform', 'Some Platform')
+    builder_key = Builder.create('some-builder', 'Some Builder')
+    return branch, platform, Builder.get(builder_key)
 
 
 def _create_build(branch, platform, builder, key_name='some-build'):
-    build_key = models.Build(key_name=key_name, branch=branch, platform=platform, builder=builder,
+    build_key = Build(key_name=key_name, branch=branch, platform=platform, builder=builder,
         buildNumber=1, revision=100, timestamp=datetime.now()).put()
-    return models.Build.get(build_key)
+    return Build.get(build_key)
 
 
 class BuildTests(DataStoreTestsBase):
@@ -218,13 +233,13 @@ class BuildTests(DataStoreTestsBase):
         branch, platform, builder = _create_some_builder()
 
         timestamp = datetime.now().replace(microsecond=0)
-        log = models.ReportLog(timestamp=timestamp, headers='some headers',
+        log = ReportLog(timestamp=timestamp, headers='some headers',
             payload='{"branch": "some-branch", "platform": "some-platform", "builder-name": "some-builder",' +
                 '"build-number": 123, "webkit-revision": 456, "timestamp": %d}' % int(mktime(timestamp.timetuple())))
 
-        self.assertThereIsNoInstanceOf(models.Build)
+        self.assertThereIsNoInstanceOf(Build)
 
-        build = models.Build.get_or_insert_from_log(log)
+        build = Build.get_or_insert_from_log(log)
         self.assertTrue(build)
         self.assertEqual(build.branch.key(), branch.key())
         self.assertEqual(build.platform.key(), platform.key())
@@ -239,43 +254,43 @@ class BuildTests(DataStoreTestsBase):
 
 class TestModelTests(DataStoreTestsBase):
     def test_update_or_insert(self):
-        branch = models.Branch.create_if_possible('some-branch', 'Some Branch')
-        platform = models.Platform.create_if_possible('some-platform', 'Some Platform')
+        branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        platform = Platform.create_if_possible('some-platform', 'Some Platform')
 
-        self.assertThereIsNoInstanceOf(models.Test)
+        self.assertThereIsNoInstanceOf(Test)
 
-        test = models.Test.update_or_insert('some-test', branch, platform)
+        test = Test.update_or_insert('some-test', branch, platform)
         self.assertTrue(test)
         self.assertEqual(test.branches, [branch.key()])
         self.assertEqual(test.platforms, [platform.key()])
         self.assertOnlyInstance(test)
 
     def test_update_or_insert_to_update(self):
-        branch = models.Branch.create_if_possible('some-branch', 'Some Branch')
-        platform = models.Platform.create_if_possible('some-platform', 'Some Platform')
-        test = models.Test.update_or_insert('some-test', branch, platform)
+        branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        test = Test.update_or_insert('some-test', branch, platform)
         self.assertOnlyInstance(test)
 
-        other_branch = models.Branch.create_if_possible('other-branch', 'Other Branch')
-        other_platform = models.Platform.create_if_possible('other-platform', 'Other Platform')
-        test = models.Test.update_or_insert('some-test', other_branch, other_platform)
+        other_branch = Branch.create_if_possible('other-branch', 'Other Branch')
+        other_platform = Platform.create_if_possible('other-platform', 'Other Platform')
+        test = Test.update_or_insert('some-test', other_branch, other_platform)
         self.assertOnlyInstance(test)
         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
 
-        test = models.Test.get(test.key())
+        test = Test.get(test.key())
         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
 
     def test_merge(self):
         branch, platform, builder = _create_some_builder()
         some_build = _create_build(branch, platform, builder)
-        some_result = models.TestResult.get_or_insert_from_parsed_json('some-test', some_build, 50)
-        some_test = models.Test.update_or_insert('some-test', branch, platform)
+        some_result = TestResult.get_or_insert_from_parsed_json('some-test', some_build, 50)
+        some_test = Test.update_or_insert('some-test', branch, platform)
 
         other_build = _create_build(branch, platform, builder, 'other-build')
-        other_result = models.TestResult.get_or_insert_from_parsed_json('other-test', other_build, 30)
-        other_test = models.Test.update_or_insert('other-test', branch, platform)
+        other_result = TestResult.get_or_insert_from_parsed_json('other-test', other_build, 30)
+        other_test = Test.update_or_insert('other-test', branch, platform)
 
         self.assertOnlyInstances([some_result, other_result])
         self.assertNotEqual(some_result.key(), other_result.key())
@@ -285,7 +300,7 @@ class TestModelTests(DataStoreTestsBase):
         self.assertOnlyInstances([some_test, other_test])
 
         some_test.merge(other_test)
-        results_for_some_test = models.TestResult.all()
+        results_for_some_test = TestResult.all()
         results_for_some_test.filter('name =', 'some-test')
         results_for_some_test = results_for_some_test.fetch(5)
         self.assertEqual(len(results_for_some_test), 2)
@@ -303,8 +318,8 @@ class TestResultTests(DataStoreTestsBase):
     def test_get_or_insert_value(self):
         branch, platform, builder = _create_some_builder()
         build = _create_build(branch, platform, builder)
-        self.assertThereIsNoInstanceOf(models.TestResult)
-        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
+        self.assertThereIsNoInstanceOf(TestResult)
+        result = TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
         self.assertOnlyInstance(result)
         self.assertEqual(result.name, 'some-test')
         self.assertEqual(result.build.key(), build.key())
@@ -317,8 +332,8 @@ class TestResultTests(DataStoreTestsBase):
     def test_get_or_insert_stat_value(self):
         branch, platform, builder = _create_some_builder()
         build = _create_build(branch, platform, builder)
-        self.assertThereIsNoInstanceOf(models.TestResult)
-        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build,
+        self.assertThereIsNoInstanceOf(TestResult)
+        result = TestResult.get_or_insert_from_parsed_json('some-test', build,
             {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
         self.assertOnlyInstance(result)
         self.assertEqual(result.name, 'some-test')
@@ -332,8 +347,8 @@ class TestResultTests(DataStoreTestsBase):
     def test_replace_to_change_test_name(self):
         branch, platform, builder = _create_some_builder()
         build = _create_build(branch, platform, builder)
-        self.assertThereIsNoInstanceOf(models.TestResult)
-        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
+        self.assertThereIsNoInstanceOf(TestResult)
+        result = TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
         self.assertOnlyInstance(result)
         self.assertEqual(result.name, 'some-test')
 
@@ -352,8 +367,8 @@ class TestResultTests(DataStoreTestsBase):
     def test_replace_to_change_test_name_with_stat_value(self):
         branch, platform, builder = _create_some_builder()
         build = _create_build(branch, platform, builder)
-        self.assertThereIsNoInstanceOf(models.TestResult)
-        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build,
+        self.assertThereIsNoInstanceOf(TestResult)
+        result = TestResult.get_or_insert_from_parsed_json('some-test', build,
             {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
         self.assertOnlyInstance(result)
         self.assertEqual(result.name, 'some-test')
@@ -374,23 +389,23 @@ class TestResultTests(DataStoreTestsBase):
     def test_replace_to_change_test_name_overrides_conflicting_result(self):
         branch, platform, builder = _create_some_builder()
         build = _create_build(branch, platform, builder)
-        self.assertThereIsNoInstanceOf(models.TestResult)
-        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 20)
+        self.assertThereIsNoInstanceOf(TestResult)
+        result = TestResult.get_or_insert_from_parsed_json('some-test', build, 20)
         self.assertOnlyInstance(result)
 
-        conflicting_result = models.TestResult.get_or_insert_from_parsed_json('other-test', build, 10)
+        conflicting_result = TestResult.get_or_insert_from_parsed_json('other-test', build, 10)
 
         new_result = result.replace_to_change_test_name('other-test')
         self.assertNotEqual(result, new_result)
         self.assertOnlyInstance(new_result)
 
         self.assertEqual(new_result.name, 'other-test')
-        self.assertEqual(models.TestResult.get(conflicting_result.key()).value, 20)
+        self.assertEqual(TestResult.get(conflicting_result.key()).value, 20)
 
 
 class ReportLogTests(DataStoreTestsBase):
     def _create_log_with_payload(self, payload):
-        return models.ReportLog(timestamp=datetime.now(), headers='some headers', payload=payload)
+        return ReportLog(timestamp=datetime.now(), headers='some headers', payload=payload)
 
     def test_parsed_payload(self):
         log = self._create_log_with_payload('')
@@ -424,7 +439,7 @@ class ReportLogTests(DataStoreTestsBase):
         log = self._create_log_with_payload('{"builder-name": "%s"}' % builder_name)
         self.assertEqual(log.builder(), None)
 
-        builder_key = models.Builder.create(builder_name, 'some password')
+        builder_key = Builder.create(builder_name, 'some password')
         log = self._create_log_with_payload('{"builder-name": "%s"}' % builder_name)
         self.assertEqual(log.builder().key(), builder_key)
 
@@ -435,7 +450,7 @@ class ReportLogTests(DataStoreTestsBase):
         log = self._create_log_with_payload('{"branch": "some-branch"}')
         self.assertEqual(log.branch(), None)
 
-        branch = models.Branch.create_if_possible("some-branch", "Some Branch")
+        branch = Branch.create_if_possible("some-branch", "Some Branch")
         log = self._create_log_with_payload('{"branch": "some-branch"}')
         self.assertEqual(log.branch().key(), branch.key())
 
@@ -446,7 +461,7 @@ class ReportLogTests(DataStoreTestsBase):
         log = self._create_log_with_payload('{"platform": "some-platform"}')
         self.assertEqual(log.platform(), None)
 
-        platform = models.Platform.create_if_possible("some-platform", "Some Platform")
+        platform = Platform.create_if_possible("some-platform", "Some Platform")
         log = self._create_log_with_payload('{"platform": "some-platform"}')
         self.assertEqual(log.platform().key(), platform.key())
 
@@ -480,31 +495,62 @@ class PersistentCacheTests(DataStoreTestsBase):
         self.testbed.init_memcache_stub()
 
     def _assert_persistent_cache(self, name, value):
-        self.assertEqual(models.PersistentCache.get_by_key_name(name).value, value)
+        self.assertEqual(PersistentCache.get_by_key_name(name).value, value)
         self.assertEqual(memcache.get(name), value)
 
     def test_set(self):
-        self.assertThereIsNoInstanceOf(models.PersistentCache)
+        self.assertThereIsNoInstanceOf(PersistentCache)
 
-        models.PersistentCache.set_cache('some-cache', 'some data')
+        PersistentCache.set_cache('some-cache', 'some data')
         self._assert_persistent_cache('some-cache', 'some data')
 
-        models.PersistentCache.set_cache('some-cache', 'some other data')
+        PersistentCache.set_cache('some-cache', 'some other data')
 
         self._assert_persistent_cache('some-cache', 'some other data')
 
     def test_get(self):
         self.assertEqual(memcache.get('some-cache'), None)
-        self.assertEqual(models.PersistentCache.get_cache('some-cache'), None)
+        self.assertEqual(PersistentCache.get_cache('some-cache'), None)
 
-        models.PersistentCache.set_cache('some-cache', 'some data')
+        PersistentCache.set_cache('some-cache', 'some data')
 
         self.assertEqual(memcache.get('some-cache'), 'some data')
-        self.assertEqual(models.PersistentCache.get_cache('some-cache'), 'some data')
+        self.assertEqual(PersistentCache.get_cache('some-cache'), 'some data')
 
         memcache.delete('some-cache')
         self.assertEqual(memcache.get('some-cache'), None)
-        self.assertEqual(models.PersistentCache.get_cache('some-cache'), 'some data')
+        self.assertEqual(PersistentCache.get_cache('some-cache'), 'some data')
+
+
+class DashboardImageTests(DataStoreTestsBase):
+    def test_needs_update(self):
+        self.assertTrue(DashboardImage.needs_update(1, 2, 3, 7))
+        self.assertTrue(DashboardImage.needs_update(1, 2, 3, 30))
+        self.assertTrue(DashboardImage.needs_update(1, 2, 3, 60))
+        self.assertTrue(DashboardImage.needs_update(1, 2, 3, 365))
+
+        image = DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 7), image='blah')
+        image.put()
+        self.assertOnlyInstance(image)
+        self.assertTrue(DashboardImage.needs_update(1, 2, 3, 7))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 30), image='blah').put()
+        self.assertFalse(DashboardImage.needs_update(1, 2, 3, 30, datetime.now() + timedelta(0, 10)))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 30), image='blah').put()
+        self.assertTrue(DashboardImage.needs_update(1, 2, 4, 30, datetime.now() + timedelta(1)))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 90), image='blah').put()
+        self.assertFalse(DashboardImage.needs_update(1, 2, 3, 90, datetime.now() + timedelta(0, 20)))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 90), image='blah').put()
+        self.assertTrue(DashboardImage.needs_update(1, 2, 4, 90, datetime.now() + timedelta(1)))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 365), image='blah').put()
+        self.assertFalse(DashboardImage.needs_update(1, 2, 3, 365, datetime.now() + timedelta(1)))
+
+        DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 365), image='blah').put()
+        self.assertTrue(DashboardImage.needs_update(1, 2, 4, 365, datetime.now() + timedelta(10)))
 
 
 if __name__ == '__main__':