mobile/android/base/background/healthreport/HealthReportGenerator.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.background.healthreport;
     7 import java.util.HashSet;
     8 import java.util.Iterator;
     9 import java.util.Set;
    11 import org.json.JSONException;
    12 import org.json.JSONObject;
    13 import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
    14 import org.mozilla.gecko.background.common.log.Logger;
    15 import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
    17 import android.database.Cursor;
    18 import android.util.SparseArray;
    20 public class HealthReportGenerator {
    21   private static final int PAYLOAD_VERSION = 3;
    23   private static final String LOG_TAG = "GeckoHealthGen";
    25   private final HealthReportStorage storage;
    26   private final DateFormatter dateFormatter;
    28   public HealthReportGenerator(HealthReportStorage storage) {
    29     this.storage = storage;
    30     this.dateFormatter = new DateFormatter();
    31   }
    33   @SuppressWarnings("static-method")
    34   protected long now() {
    35     return System.currentTimeMillis();
    36   }
    38   /**
    39    * Ensure that you have initialized the Locale to your satisfaction
    40    * prior to calling this method.
    41    *
    42    * @return null if no environment could be computed, or else the resulting document.
    43    * @throws JSONException if there was an error adding environment data to the resulting document.
    44    */
    45   public JSONObject generateDocument(long since, long lastPingTime, String profilePath) throws JSONException {
    46     Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime);
    47     Logger.pii(LOG_TAG, "Generating for profile " + profilePath);
    49     ProfileInformationCache cache = new ProfileInformationCache(profilePath);
    50     if (!cache.restoreUnlessInitialized()) {
    51       Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
    52       return null;
    53     }
    54     Environment current = EnvironmentBuilder.getCurrentEnvironment(cache);
    55     return generateDocument(since, lastPingTime, current);
    56   }
    58   /**
    59    * The document consists of:
    60    *
    61    *<ul>
    62    *<li>Basic metadata: last ping time, current ping time, version.</li>
    63    *<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified,
    64    * and others are deltas from current.</li>
    65    *<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li>
    66    *</ul>
    67    *
    68    * <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>.
    69    * @throws JSONException if there was an error adding environment data to the resulting document.
    70    */
    71   public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException {
    72     final String currentHash = currentEnvironment.getHash();
    74     Logger.debug(LOG_TAG, "Current environment hash: " + currentHash);
    75     if (currentHash == null) {
    76       Logger.warn(LOG_TAG, "Current hash is null; aborting.");
    77       return null;
    78     }
    80     // We want to map field IDs to some strings as we go.
    81     SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
    83     JSONObject document = new JSONObject();
    85     if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
    86       document.put("lastPingDate", dateFormatter.getDateString(lastPingTime));
    87     }
    89     document.put("thisPingDate", dateFormatter.getDateString(now()));
    90     document.put("version", PAYLOAD_VERSION);
    92     document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
    93     document.put("data", getDataJSON(currentEnvironment, envs, since));
    95     return document;
    96   }
    98   protected JSONObject getDataJSON(Environment currentEnvironment,
    99                                    SparseArray<Environment> envs, long since) throws JSONException {
   100     SparseArray<Field> fields = storage.getFieldsByID();
   102     JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since);
   104     JSONObject last = new JSONObject();
   106     JSONObject data = new JSONObject();
   107     data.put("days", days);
   108     data.put("last", last);
   109     return data;
   110   }
   112   protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException {
   113     if (Logger.shouldLogVerbose(LOG_TAG)) {
   114       for (int i = 0; i < envs.size(); ++i) {
   115         Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash());
   116       }
   117     }
   119     JSONObject days = new JSONObject();
   120     Cursor cursor = storage.getRawEventsSince(since);
   121     try {
   122       if (!cursor.moveToFirst()) {
   123         return days;
   124       }
   126       // A classic walking partition.
   127       // Columns are "date", "env", "field", "value".
   128       // Note that we care about the type (integer, string) and kind
   129       // (last/counter, discrete) of each field.
   130       // Each field will be accessed once for each date/env pair, so
   131       // Field memoizes these facts.
   132       // We also care about which measurement contains each field.
   133       int lastDate  = -1;
   134       int lastEnv   = -1;
   135       JSONObject dateObject = null;
   136       JSONObject envObject = null;
   138       while (!cursor.isAfterLast()) {
   139         int cEnv = cursor.getInt(1);
   140         if (cEnv == -1 ||
   141             (cEnv != lastEnv &&
   142              envs.indexOfKey(cEnv) < 0)) {
   143           Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping.");
   144           cursor.moveToNext();
   145           continue;
   146         }
   148         int cDate  = cursor.getInt(0);
   149         int cField = cursor.getInt(2);
   151         Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField);
   152         boolean dateChanged = cDate != lastDate;
   153         boolean envChanged = cEnv != lastEnv;
   155         if (dateChanged) {
   156           if (dateObject != null) {
   157             days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
   158           }
   159           dateObject = new JSONObject();
   160           lastDate = cDate;
   161         }
   163         if (dateChanged || envChanged) {
   164           envObject = new JSONObject();
   165           // This is safe because we checked above that cEnv is valid.
   166           dateObject.put(envs.get(cEnv).getHash(), envObject);
   167           lastEnv = cEnv;
   168         }
   170         final Field field = fields.get(cField);
   171         JSONObject measurement = envObject.optJSONObject(field.measurementName);
   172         if (measurement == null) {
   173           // We will never have more than one measurement version within a
   174           // single environment -- to do so involves changing the build ID. And
   175           // even if we did, we have no way to represent it. So just build the
   176           // output object once.
   177           measurement = new JSONObject();
   178           measurement.put("_v", field.measurementVersion);
   179           envObject.put(field.measurementName, measurement);
   180         }
   182         // How we record depends on the type of the field, so we
   183         // break this out into a separate method for clarity.
   184         recordMeasurementFromCursor(field, measurement, cursor);
   186         cursor.moveToNext();
   187         continue;
   188       }
   189       days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
   190     } finally {
   191       cursor.close();
   192     }
   193     return days;
   194   }
   196   /**
   197    * Return the {@link JSONObject} parsed from the provided index of the given
   198    * cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or
   199    * string <code>"null"</code> is present at that index.
   200    */
   201   private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException {
   202     if (cursor.isNull(index)) {
   203       return JSONObject.NULL;
   204     }
   205     final String value = cursor.getString(index);
   206     if ("null".equals(value)) {
   207       return JSONObject.NULL;
   208     }
   209     return new JSONObject(value);
   210   }
   212   protected static void recordMeasurementFromCursor(final Field field,
   213                                              JSONObject measurement,
   214                                              Cursor cursor)
   215                                                            throws JSONException {
   216     if (field.isDiscreteField()) {
   217       // Discrete counted. Increment the named counter.
   218       if (field.isCountedField()) {
   219         if (!field.isStringField()) {
   220           throw new IllegalStateException("Unable to handle non-string counted types.");
   221         }
   222         HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3));
   223         return;
   224       }
   226       // Discrete string or integer. Append it.
   227       if (field.isStringField()) {
   228         HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
   229         return;
   230       }
   231       if (field.isJSONField()) {
   232         HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3));
   233         return;
   234       }
   235       if (field.isIntegerField()) {
   236         HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
   237         return;
   238       }
   239       throw new IllegalStateException("Unknown field type: " + field.flags);
   240     }
   242     // Non-discrete -- must be LAST or COUNTER, so just accumulate the value.
   243     if (field.isStringField()) {
   244       measurement.put(field.fieldName, cursor.getString(3));
   245       return;
   246     }
   247     if (field.isJSONField()) {
   248       measurement.put(field.fieldName, getJSONAtIndex(cursor, 3));
   249       return;
   250     }
   251     measurement.put(field.fieldName, cursor.getLong(3));
   252   }
   254   public static JSONObject getEnvironmentsJSON(Environment currentEnvironment,
   255                                                SparseArray<Environment> envs) throws JSONException {
   256     JSONObject environments = new JSONObject();
   258     // Always do this, even if it hasn't recorded anything in the DB.
   259     environments.put("current", jsonify(currentEnvironment, null));
   261     String currentHash = currentEnvironment.getHash();
   262     for (int i = 0; i < envs.size(); i++) {
   263       Environment e = envs.valueAt(i);
   264       if (currentHash.equals(e.getHash())) {
   265         continue;
   266       }
   267       environments.put(e.getHash(), jsonify(e, currentEnvironment));
   268     }
   269     return environments;
   270   }
   272   public static JSONObject jsonify(Environment e, Environment current) throws JSONException {
   273     JSONObject age = getProfileAge(e, current);
   274     JSONObject sysinfo = getSysInfo(e, current);
   275     JSONObject gecko = getGeckoInfo(e, current);
   276     JSONObject appinfo = getAppInfo(e, current);
   277     JSONObject counts = getAddonCounts(e, current);
   279     JSONObject out = new JSONObject();
   280     if (age != null)
   281       out.put("org.mozilla.profile.age", age);
   282     if (sysinfo != null)
   283       out.put("org.mozilla.sysinfo.sysinfo", sysinfo);
   284     if (gecko != null)
   285       out.put("geckoAppInfo", gecko);
   286     if (appinfo != null)
   287       out.put("org.mozilla.appInfo.appinfo", appinfo);
   288     if (counts != null)
   289       out.put("org.mozilla.addons.counts", counts);
   291     JSONObject active = getActiveAddons(e, current);
   292     if (active != null)
   293       out.put("org.mozilla.addons.active", active);
   295     if (current == null) {
   296       out.put("hash", e.getHash());
   297     }
   298     return out;
   299   }
   301   private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException {
   302     JSONObject age = new JSONObject();
   303     int changes = 0;
   304     if (current == null || current.profileCreation != e.profileCreation) {
   305       age.put("profileCreation", e.profileCreation);
   306       changes++;
   307     }
   308     if (current != null && changes == 0) {
   309       return null;
   310     }
   311     age.put("_v", 1);
   312     return age;
   313   }
   315   private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException {
   316     JSONObject sysinfo = new JSONObject();
   317     int changes = 0;
   318     if (current == null || current.cpuCount != e.cpuCount) {
   319       sysinfo.put("cpuCount", e.cpuCount);
   320       changes++;
   321     }
   322     if (current == null || current.memoryMB != e.memoryMB) {
   323       sysinfo.put("memoryMB", e.memoryMB);
   324       changes++;
   325     }
   326     if (current == null || !current.architecture.equals(e.architecture)) {
   327       sysinfo.put("architecture", e.architecture);
   328       changes++;
   329     }
   330     if (current == null || !current.sysName.equals(e.sysName)) {
   331       sysinfo.put("name", e.sysName);
   332       changes++;
   333     }
   334     if (current == null || !current.sysVersion.equals(e.sysVersion)) {
   335       sysinfo.put("version", e.sysVersion);
   336       changes++;
   337     }
   338     if (current != null && changes == 0) {
   339       return null;
   340     }
   341     sysinfo.put("_v", 1);
   342     return sysinfo;
   343   }
   345   private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException {
   346     JSONObject gecko = new JSONObject();
   347     int changes = 0;
   348     if (current == null || !current.vendor.equals(e.vendor)) {
   349       gecko.put("vendor", e.vendor);
   350       changes++;
   351     }
   352     if (current == null || !current.appName.equals(e.appName)) {
   353       gecko.put("name", e.appName);
   354       changes++;
   355     }
   356     if (current == null || !current.appID.equals(e.appID)) {
   357       gecko.put("id", e.appID);
   358       changes++;
   359     }
   360     if (current == null || !current.appVersion.equals(e.appVersion)) {
   361       gecko.put("version", e.appVersion);
   362       changes++;
   363     }
   364     if (current == null || !current.appBuildID.equals(e.appBuildID)) {
   365       gecko.put("appBuildID", e.appBuildID);
   366       changes++;
   367     }
   368     if (current == null || !current.platformVersion.equals(e.platformVersion)) {
   369       gecko.put("platformVersion", e.platformVersion);
   370       changes++;
   371     }
   372     if (current == null || !current.platformBuildID.equals(e.platformBuildID)) {
   373       gecko.put("platformBuildID", e.platformBuildID);
   374       changes++;
   375     }
   376     if (current == null || !current.os.equals(e.os)) {
   377       gecko.put("os", e.os);
   378       changes++;
   379     }
   380     if (current == null || !current.xpcomabi.equals(e.xpcomabi)) {
   381       gecko.put("xpcomabi", e.xpcomabi);
   382       changes++;
   383     }
   384     if (current == null || !current.updateChannel.equals(e.updateChannel)) {
   385       gecko.put("updateChannel", e.updateChannel);
   386       changes++;
   387     }
   388     if (current != null && changes == 0) {
   389       return null;
   390     }
   391     gecko.put("_v", 1);
   392     return gecko;
   393   }
   395   // Null-safe string comparison.
   396   private static boolean stringsDiffer(final String a, final String b) {
   397     if (a == null) {
   398       return b != null;
   399     }
   400     return !a.equals(b);
   401   }
   403   private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException {
   404     JSONObject appinfo = new JSONObject();
   406     Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash);
   408     // Is the environment in question newer than the diff target, or is
   409     // there no diff target?
   410     final boolean outdated = current == null ||
   411                              e.version > current.version;
   413     // Is the environment in question a different version (lower or higher),
   414     // or is there no diff target?
   415     final boolean differ = outdated || current.version > e.version;
   417     // Always produce an output object if there's a version mismatch or this
   418     // isn't a diff. Otherwise, track as we go if there's any difference.
   419     boolean changed = differ;
   421     switch (e.version) {
   422     // There's a straightforward correspondence between environment versions
   423     // and appinfo versions.
   424     case 2:
   425       appinfo.put("_v", 3);
   426       break;
   427     case 1:
   428       appinfo.put("_v", 2);
   429       break;
   430     default:
   431       Logger.warn(LOG_TAG, "Unknown environment version: " + e.version);
   432       return appinfo;
   433     }
   435     switch (e.version) {
   436     case 2:
   437       if (populateAppInfoV2(appinfo, e, current, outdated)) {
   438         changed = true;
   439       }
   440       // Fall through.
   442     case 1:
   443       // There is no older version than v1, so don't check outdated.
   444       if (populateAppInfoV1(e, current, appinfo)) {
   445         changed = true;
   446       }
   447     }
   449     if (!changed) {
   450       return null;
   451     }
   453     return appinfo;
   454   }
   456   private static boolean populateAppInfoV1(Environment e,
   457                                            Environment current,
   458                                            JSONObject appinfo)
   459     throws JSONException {
   460     boolean changes = false;
   461     if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
   462       appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
   463       changes = true;
   464     }
   466     if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
   467       appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
   468       changes = true;
   469     }
   471     return changes;
   472   }
   474   private static boolean populateAppInfoV2(JSONObject appinfo,
   475                                            Environment e,
   476                                            Environment current,
   477                                            final boolean outdated)
   478     throws JSONException {
   479     boolean changes = false;
   480     if (outdated ||
   481         stringsDiffer(current.osLocale, e.osLocale)) {
   482       appinfo.put("osLocale", e.osLocale);
   483       changes = true;
   484     }
   486     if (outdated ||
   487         stringsDiffer(current.appLocale, e.appLocale)) {
   488       appinfo.put("appLocale", e.appLocale);
   489       changes = true;
   490     }
   492     if (outdated ||
   493         stringsDiffer(current.distribution, e.distribution)) {
   494       appinfo.put("distribution", e.distribution);
   495       changes = true;
   496     }
   498     if (outdated ||
   499         current.acceptLangSet != e.acceptLangSet) {
   500       appinfo.put("acceptLangIsUserSet", e.acceptLangSet);
   501       changes = true;
   502     }
   503     return changes;
   504   }
   506   private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException {
   507     JSONObject counts = new JSONObject();
   508     int changes = 0;
   509     if (current == null || current.extensionCount != e.extensionCount) {
   510       counts.put("extension", e.extensionCount);
   511       changes++;
   512     }
   513     if (current == null || current.pluginCount != e.pluginCount) {
   514       counts.put("plugin", e.pluginCount);
   515       changes++;
   516     }
   517     if (current == null || current.themeCount != e.themeCount) {
   518       counts.put("theme", e.themeCount);
   519       changes++;
   520     }
   521     if (current != null && changes == 0) {
   522       return null;
   523     }
   524     counts.put("_v", 1);
   525     return counts;
   526   }
   528   /**
   529    * Compute the *tree* difference set between the two objects. If the two
   530    * objects are identical, returns <code>null</code>. If <code>from</code> is
   531    * <code>null</code>, returns <code>to</code>. If <code>to</code> is
   532    * <code>null</code>, behaves as if <code>to</code> were an empty object.
   533    *
   534    * (Note that this method does not check for {@link JSONObject#NULL}, because
   535    * by definition it can't be provided as input to this method.)
   536    *
   537    * This behavior is intended to simplify life for callers: a missing object
   538    * can be viewed as (and behaves as) an empty map, to a useful extent, rather
   539    * than throwing an exception.
   540    *
   541    * @param from
   542    *          a JSONObject.
   543    * @param to
   544    *          a JSONObject.
   545    * @param includeNull
   546    *          if true, keys present in <code>from</code> but not in
   547    *          <code>to</code> are included as {@link JSONObject#NULL} in the
   548    *          output.
   549    *
   550    * @return a JSONObject, or null if the two objects are identical.
   551    * @throws JSONException
   552    *           should not occur, but...
   553    */
   554   public static JSONObject diff(JSONObject from,
   555                                 JSONObject to,
   556                                 boolean includeNull) throws JSONException {
   557     if (from == null) {
   558       return to;
   559     }
   561     if (to == null) {
   562       return diff(from, new JSONObject(), includeNull);
   563     }
   565     JSONObject out = new JSONObject();
   567     HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length())
   568                                          : null;
   570     @SuppressWarnings("unchecked")
   571     Iterator<String> it = to.keys();
   572     while (it.hasNext()) {
   573       String key = it.next();
   575       // Track these as we go if we'll need them later.
   576       if (includeNull) {
   577         toKeys.add(key);
   578       }
   580       Object value = to.get(key);
   581       if (!from.has(key)) {
   582         // It must be new.
   583         out.put(key, value);
   584         continue;
   585       }
   587       // Not new? Then see if it changed.
   588       Object old = from.get(key);
   590       // Two JSONObjects should be diffed.
   591       if (old instanceof JSONObject && value instanceof JSONObject) {
   592         JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value),
   593                                     includeNull);
   594         // No change? No output.
   595         if (innerDiff == null) {
   596           continue;
   597         }
   599         // Otherwise include the diff.
   600         out.put(key, innerDiff);
   601         continue;
   602       }
   604       // A regular value, or a type change. Only skip if they're the same.
   605       if (value.equals(old)) {
   606         continue;
   607       }
   608       out.put(key, value);
   609     }
   611     // Now -- if requested -- include any removed keys.
   612     if (includeNull) {
   613       Set<String> fromKeys = HealthReportUtils.keySet(from);
   614       fromKeys.removeAll(toKeys);
   615       for (String notPresent : fromKeys) {
   616         out.put(notPresent, JSONObject.NULL);
   617       }
   618     }
   620     if (out.length() == 0) {
   621       return null;
   622     }
   623     return out;
   624   }
   626   private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException {
   627     // Just return the current add-on set, with a version annotation.
   628     // To do so requires copying.
   629     if (current == null) {
   630       JSONObject out = e.getNonIgnoredAddons();
   631       if (out == null) {
   632         Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}.");
   633         out = new JSONObject();        // So that we always return something.
   634       }
   635       out.put("_v", 1);
   636       return out;
   637     }
   639     // Otherwise, return the diff.
   640     JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true);
   641     if (diff == null) {
   642       return null;
   643     }
   644     if (diff == e.addons) {
   645       // Again, needs to copy.
   646       return getActiveAddons(e, null);
   647     }
   649     diff.put("_v", 1);
   650     return diff;
   651   }
   652 }

mercurial