Bug 878303 - Part 1: implement TYPE_COUNTED_STRING_DISCRETE. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Mon, 03 Jun 2013 14:12:00 -0700
changeset 134127 a31f790f06814d1faa6289577292ace90a2a5c0b
parent 134126 76629dcc62991228708181a74c1214e0ed2d97dc
child 134128 5ccd0592e074a249595230c7b964727afec6df01
push id29067
push userryanvm@gmail.com
push dateWed, 05 Jun 2013 20:37:20 +0000
treeherdermozilla-inbound@72fbfb2f8e51 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs878303
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 878303 - Part 1: implement TYPE_COUNTED_STRING_DISCRETE. r=nalexander
mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
mobile/android/base/background/healthreport/HealthReportGenerator.java
mobile/android/base/background/healthreport/HealthReportStorage.java
mobile/android/base/background/healthreport/HealthReportUtils.java
--- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
+++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
@@ -180,17 +180,17 @@ public class HealthReportDatabaseStorage
     this.fields.clear();
     this.envs.clear();
     this.measurementVersions.clear();
   }
 
   protected final HealthReportSQLiteOpenHelper helper;
 
   public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper {
-    public static final int CURRENT_VERSION = 3;
+    public static final int CURRENT_VERSION = 4;
     public static final String LOG_TAG = "HealthReportSQL";
 
     /**
      * A little helper to avoid SQLiteOpenHelper misbehaving on Android 2.1.
      * Partly cribbed from
      * <http://stackoverflow.com/questions/5332328/sqliteopenhelper-problem-with-fully-qualified-db-path-name>.
      */
     public static class AbsolutePathContext extends ContextWrapper {
@@ -382,28 +382,36 @@ public class HealthReportDatabaseStorage
                  "                     UNIQUE (body) " +
                  ")");
 
       db.execSQL("ALTER TABLE environments ADD COLUMN addonsID INTEGER REFERENCES addons(id) ON DELETE RESTRICT");
 
       createAddonsEnvironmentsView(db);
     }
 
+    private void upgradeDatabaseFrom3To4(SQLiteDatabase db) {
+      // Update search measurements to use a different type.
+      db.execSQL("UPDATE OR IGNORE fields SET flags = " + Field.TYPE_COUNTED_STRING_DISCRETE +
+                 " WHERE measurement IN (SELECT id FROM measurements WHERE name = 'org.mozilla.searches.counts')");
+    }
+
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
       if (oldVersion >= newVersion) {
         return;
       }
 
       Logger.info(LOG_TAG, "onUpgrade: from " + oldVersion + " to " + newVersion + ".");
       try {
         db.beginTransaction();
         switch (oldVersion) {
         case 2:
           upgradeDatabaseFrom2To3(db);
+        case 3:
+          upgradeDatabaseFrom3To4(db);
         }
         db.setTransactionSuccessful();
       } catch (Exception e) {
         Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
         throw new RuntimeException(e);
       } finally {
         db.endTransaction();
       }
--- a/mobile/android/base/background/healthreport/HealthReportGenerator.java
+++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java
@@ -166,43 +166,65 @@ public class HealthReportGenerator {
           // We will never have more than one measurement version within a
           // single environment -- to do so involves changing the build ID. And
           // even if we did, we have no way to represent it. So just build the
           // output object once.
           measurement = new JSONObject();
           measurement.put("_v", field.measurementVersion);
           envObject.put(field.measurementName, measurement);
         }
-        if (field.isDiscreteField()) {
-          if (field.isStringField()) {
-            HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
-          } else if (field.isIntegerField()) {
-            HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
-          } else {
-            // Uh oh!
-            throw new IllegalStateException("Unknown field type: " + field.flags);
-          }
-        } else {
-          if (field.isStringField()) {
-            measurement.put(field.fieldName, cursor.getString(3));
-          } else {
-            measurement.put(field.fieldName, cursor.getLong(3));
-          }
-        }
+
+        // How we record depends on the type of the field, so we
+        // break this out into a separate method for clarity.
+        recordMeasurementFromCursor(field, measurement, cursor);
 
         cursor.moveToNext();
         continue;
       }
       days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject);
     } finally {
       cursor.close();
     }
     return days;
   }
 
+  protected static void recordMeasurementFromCursor(final Field field,
+                                             JSONObject measurement,
+                                             Cursor cursor)
+                                                           throws JSONException {
+    if (field.isDiscreteField()) {
+      // Discrete counted. Increment the named counter.
+      if (field.isCountedField()) {
+        if (!field.isStringField()) {
+          throw new IllegalStateException("Unable to handle non-string counted types.");
+        }
+        HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3));
+        return;
+      }
+
+      // Discrete string or integer. Append it.
+      if (field.isStringField()) {
+        HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
+        return;
+      }
+      if (field.isIntegerField()) {
+        HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
+        return;
+      }
+      throw new IllegalStateException("Unknown field type: " + field.flags);
+    }
+
+    // Non-discrete -- must be LAST or COUNTER, so just accumulate the value.
+    if (field.isStringField()) {
+      measurement.put(field.fieldName, cursor.getString(3));
+      return;
+    }
+    measurement.put(field.fieldName, cursor.getLong(3));
+  }
+
   public static JSONObject getEnvironmentsJSON(Environment currentEnvironment,
                                                SparseArray<Environment> envs) throws JSONException {
     JSONObject environments = new JSONObject();
 
     // Always do this, even if it hasn't recorded anything in the DB.
     environments.put("current", jsonify(currentEnvironment, null));
 
     String currentHash = currentEnvironment.getHash();
--- a/mobile/android/base/background/healthreport/HealthReportStorage.java
+++ b/mobile/android/base/background/healthreport/HealthReportStorage.java
@@ -29,23 +29,27 @@ public interface HealthReportStorage {
 
     protected static final int FLAG_INTEGER  = 1 << 0;
     protected static final int FLAG_STRING   = 1 << 1;
 
     protected static final int FLAG_DISCRETE = 1 << 8;
     protected static final int FLAG_LAST     = 1 << 9;
     protected static final int FLAG_COUNTER  = 1 << 10;
 
+    protected static final int FLAG_COUNTED  = 1 << 14;
+
     public static final int TYPE_INTEGER_DISCRETE = FLAG_INTEGER | FLAG_DISCRETE;
     public static final int TYPE_INTEGER_LAST     = FLAG_INTEGER | FLAG_LAST;
     public static final int TYPE_INTEGER_COUNTER  = FLAG_INTEGER | FLAG_COUNTER;
 
     public static final int TYPE_STRING_DISCRETE  = FLAG_STRING | FLAG_DISCRETE;
     public static final int TYPE_STRING_LAST      = FLAG_STRING | FLAG_LAST;
 
+    public static final int TYPE_COUNTED_STRING_DISCRETE = FLAG_COUNTED | TYPE_STRING_DISCRETE;
+
     protected int fieldID = UNKNOWN_TYPE_OR_FIELD_ID;
     protected int flags;
 
     protected final String measurementName;
     protected final String measurementVersion;
     protected final String fieldName;
 
     public Field(String mName, int mVersion, String fieldName, int type) {
@@ -67,16 +71,25 @@ public interface HealthReportStorage {
 
     public boolean isStringField() {
       return (this.flags & FLAG_STRING) > 0;
     }
 
     public boolean isDiscreteField() {
       return (this.flags & FLAG_DISCRETE) > 0;
     }
+
+    /**
+     * True if the accrued values are intended to be bucket-counted. For strings,
+     * each discrete value will name a bucket, with the number of instances per
+     * day being the value in the bucket.
+     */
+    public boolean isCountedField() {
+      return (this.flags & FLAG_COUNTED) > 0;
+    }
   }
 
   /**
    * Close open storage handles and otherwise finish up.
    */
   public void close();
 
   /**
--- a/mobile/android/base/background/healthreport/HealthReportUtils.java
+++ b/mobile/android/base/background/healthreport/HealthReportUtils.java
@@ -105,11 +105,42 @@ public class HealthReportUtils {
     if (dest instanceof JSONArray) {
       ((JSONArray) dest).put(value);
       return;
     }
     JSONArray arr = new JSONArray();
     arr.put(dest);
     arr.put(value);
     o.put(key, arr);
-    return;
+  }
+
+  /**
+   * Accumulate counts for how often each provided value occurs.
+   *
+   * <code>
+   *   HealthReportUtils.count(o, "foo", "bar");
+   * </code>
+   *
+   * will change
+   *
+   * <pre>
+   *   {"foo", {"bar": 1}}
+   * </pre>
+   *
+   * into
+   *
+   * <pre>
+   *   {"foo", {"bar": 2}}
+   * </pre>
+   *
+   */
+  public static void count(JSONObject o, String key,
+                           String value) throws JSONException {
+    if (!o.has(key)) {
+      JSONObject counts = new JSONObject();
+      counts.put(value, 1);
+      o.put(key, counts);
+      return;
+    }
+    JSONObject dest = o.getJSONObject(key);
+    dest.put(value, dest.optInt(value, 0) + 1);
   }
 }