Bug 928969 - Telemetry infrastructure for Shumway. r=till
authorYury Delendik <ydelendik@mozilla.com>
Mon, 21 Oct 2013 10:59:43 -0500
changeset 151853 8d7026e79fae67e7ffe4a67f31c38e6f58a070c9
parent 151852 124fc1fcd3ebbe8484d0a5fb66bba6336daa3300
child 151854 7a457cfcaa9ad94ff64dd9fa5df52e94b622c806
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerstill
bugs928969
milestone27.0a1
Bug 928969 - Telemetry infrastructure for Shumway. r=till
browser/extensions/shumway/content/ShumwayStreamConverter.jsm
browser/extensions/shumway/content/ShumwayTelemetry.jsm
browser/extensions/shumway/content/shumway-worker.js
browser/extensions/shumway/content/shumway.js
browser/extensions/shumway/content/version.txt
browser/extensions/shumway/content/web/avm-sandbox.js
--- a/browser/extensions/shumway/content/ShumwayStreamConverter.jsm
+++ b/browser/extensions/shumway/content/ShumwayStreamConverter.jsm
@@ -36,16 +36,19 @@ const MAX_CLIPBOARD_DATA_SIZE = 8000;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/NetUtil.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
   'resource://gre/modules/PrivateBrowsingUtils.jsm');
 
+XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
+  'resource://shumway/ShumwayTelemetry.jsm');
+
 let appInfo = Cc['@mozilla.org/xre/app-info;1'].getService(Ci.nsIXULAppInfo);
 let Svc = {};
 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
                                    '@mozilla.org/mime;1', 'nsIMIMEService');
 
 let profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
 
 function getBoolPref(pref, def) {
@@ -191,16 +194,22 @@ function ChromeActions(url, window, docu
   this.baseUrl = url;
   this.isOverlay = false;
   this.isPausedAtStart = false;
   this.window = window;
   this.document = document;
   this.externalComInitialized = false;
   this.allowScriptAccess = false;
   this.crossdomainRequestsCache = Object.create(null);
+  this.telemetry = {
+    startTime: Date.now(),
+    features: [],
+    errors: [],
+    pageIndex: 0
+  };
 }
 
 ChromeActions.prototype = {
   getBoolPref: function (data) {
     if (!/^shumway\./.test(data.pref)) {
       return null;
     }
     return getBoolPref(data.pref, data.def);
@@ -329,22 +338,24 @@ ChromeActions.prototype = {
         performXHR();
       } else {
         log("data access id prohibited to " + url + " from " + baseUrl);
         win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
           error: "only original swf file or file from the same origin loading supported"}, "*");
       }
     });
   },
-  fallback: function() {
+  fallback: function(automatic) {
     var obj = this.window.frameElement;
     var doc = obj.ownerDocument;
     var e = doc.createEvent("CustomEvent");
     e.initCustomEvent("MozPlayPlugin", true, true, null);
     obj.dispatchEvent(e);
+
+    ShumwayTelemetry.onFallback(!automatic);
   },
   setClipboard: function (data) {
     if (typeof data !== 'string' ||
         data.length > MAX_CLIPBOARD_DATA_SIZE ||
         !this.document.hasFocus()) {
       return;
     }
     // TODO other security checks?
@@ -360,16 +371,55 @@ ChromeActions.prototype = {
     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
     clipboard.copyString(data);
   },
   endActivation: function () {
     if (ActivationQueue.currentNonActive === this) {
       ActivationQueue.activateNext();
     }
   },
+  reportTelemetry: function (data) {
+    var topic = data.topic;
+    switch (topic) {
+    case 'firstFrame':
+      var time = Date.now() - this.telemetry.startTime;
+      ShumwayTelemetry.onFirstFrame(time);
+      break;
+    case 'parseInfo':
+      ShumwayTelemetry.onParseInfo({
+        parseTime: +data.parseTime,
+        size: +data.bytesTotal,
+        swfVersion: data.swfVersion|0,
+        frameRate: +data.frameRate,
+        width: data.width|0,
+        height: data.height|0,
+        bannerType: data.bannerType|0,
+        isAvm2: !!data.isAvm2
+      });
+      break;
+    case 'feature':
+      var featureType = data.feature|0;
+      var MIN_FEATURE_TYPE = 0, MAX_FEATURE_TYPE = 999;
+      if (featureType >= MIN_FEATURE_TYPE && featureType <= MAX_FEATURE_TYPE &&
+          !this.telemetry.features[featureType]) {
+        this.telemetry.features[featureType] = true; // record only one feature per SWF
+        ShumwayTelemetry.onFeature(featureType);
+      }
+      break;
+    case 'error':
+      var errorType = data.error|0;
+      var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2;
+      if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE &&
+          !this.telemetry.errors[errorType]) {
+        this.telemetry.errors[errorType] = true; // record only one report per SWF
+        ShumwayTelemetry.onError(errorType);
+      }
+      break;
+    }
+  },
   externalCom: function (data) {
     if (!this.allowScriptAccess)
       return;
 
     // TODO check security ?
     var parentWindow = this.window.parent.wrappedJSObject;
     var embedTag = this.embedTag.wrappedJSObject;
     switch (data.action) {
@@ -447,16 +497,24 @@ var ActivationQueue = {
     return this.nonActive[this.initializing];
   },
   enqueue: function ActivationQueue_enqueue(actions) {
     this.nonActive.push(actions);
     if (this.nonActive.length === 1) {
       this.activateNext();
     }
   },
+  findLastOnPage: function ActivationQueue_findLastOnPage(baseUrl) {
+    for (var i = this.nonActive.length - 1; i >= 0; i--) {
+      if (this.nonActive[i].baseUrl === baseUrl) {
+        return this.nonActive[i];
+      }
+    }
+    return null;
+  },
   activateNext: function ActivationQueue_activateNext() {
     function weightInstance(actions) {
       // set of heuristics for find the most important instance to load
       var weight = 0;
       // using linear distance to the top-left of the view area
       if (actions.embedTag) {
         var window = actions.window;
         var clientRect = actions.embedTag.getBoundingClientRect();
@@ -809,20 +867,31 @@ ShumwayStreamConverterBase.prototype = {
       onStopRequest: function() {
         var domWindow = getDOMWindow(channel);
         if (domWindow.document.documentURIObject.equals(channel.originalURI)) {
           // Double check the url is still the correct one.
           let actions = converter.createChromeActions(domWindow,
                                                       domWindow.document,
                                                       converter.getUrlHint(originalURI));
           if (!isShumwayEnabledFor(actions)) {
-            actions.fallback();
+            actions.fallback(true);
             return;
           }
 
+          // Report telemetry on amount of swfs on the page
+          if (actions.isOverlay) {
+            // Looking for last actions with same baseUrl
+            var prevPageActions = ActivationQueue.findLastOnPage(actions.baseUrl);
+            var pageIndex = !prevPageActions ? 1 : (prevPageActions.telemetry.pageIndex + 1);
+            actions.telemetry.pageIndex = pageIndex;
+            ShumwayTelemetry.onPageIndex(pageIndex);
+          } else {
+            ShumwayTelemetry.onPageIndex(0);
+          }
+
           actions.activationCallback = function(domWindow, isSimpleMode) {
             delete this.activationCallback;
             activateShumwayScripts(domWindow, isSimpleMode);
           }.bind(actions, domWindow, isSimpleMode);
           ActivationQueue.enqueue(actions);
 
           let requestListener = new RequestListener(actions);
           domWindow.addEventListener('shumway.message', function(event) {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shumway/content/ShumwayTelemetry.jsm
@@ -0,0 +1,73 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* Copyright 2013 Mozilla Foundation
+ *
+ * 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.
+ */
+/* jshint esnext:true */
+
+'use strict';
+
+this.EXPORTED_SYMBOLS = ['ShumwayTelemetry'];
+
+const Cu = Components.utils;
+Cu.import('resource://gre/modules/Services.jsm');
+
+const BANNER_SIZES = [
+  "88x31", "120x60", "120x90", "120x240", "120x600", "125x125", "160x600",
+  "180x150", "234x60", "240x400", "250x250", "300x100", "300x250", "300x600",
+  "300x1050", "336x280", "468x60", "550x480", "720x100", "728x90", "970x90",
+  "970x250"];
+
+function getBannerType(width, height) {
+  return BANNER_SIZES.indexOf(width + 'x' + height) + 1;
+}
+
+this.ShumwayTelemetry = {
+  onFirstFrame: function (timeToDisplay) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_TIME_TO_VIEW_MS");
+    histogram.add(timeToDisplay);
+  },
+  onParseInfo: function (parseInfo) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_PARSING_MS");
+    histogram.add(parseInfo.parseTime);
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_SIZE_KB");
+    histogram.add(parseInfo.size / 1024);
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_VERSION");
+    histogram.add(parseInfo.swfVersion);
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_FRAME_RATE");
+    histogram.add(parseInfo.frameRate);
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_AREA");
+    histogram.add(parseInfo.width * parseInfo.height);
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_BANNER");
+    histogram.add(getBannerType(parseInfo.width, parseInfo.height));
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_AVM2");
+    histogram.add(parseInfo.isAvm2);
+  },
+  onError: function (errorType) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_ERROR");
+    histogram.add(errorType);
+  },
+  onPageIndex: function (pageIndex) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_SWF_INDEX_ON_PAGE");
+    histogram.add(pageIndex);
+  },
+  onFeature: function (featureType) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_FEATURE_USED");
+    histogram.add(featureType);
+  },
+  onFallback: function (userAction) {
+    var histogram = Services.telemetry.getHistogramById("SHUMWAY_FALLBACK");
+    histogram.add(userAction);
+  }
+};
--- a/browser/extensions/shumway/content/shumway-worker.js
+++ b/browser/extensions/shumway/content/shumway-worker.js
@@ -430,16 +430,22 @@ QuadTree.prototype._subdivide = function
   var midX = this.x + halfWidth;
   var midY = this.y + halfHeight;
   var level = this.level + 1;
   this.nodes[0] = new QuadTree(midX, this.y, halfWidth, halfHeight, level);
   this.nodes[1] = new QuadTree(this.x, this.y, halfWidth, halfHeight, level);
   this.nodes[2] = new QuadTree(this.x, midY, halfWidth, halfHeight, level);
   this.nodes[3] = new QuadTree(midX, midY, halfWidth, halfHeight, level);
 };
+var EXTERNAL_INTERFACE_FEATURE = 1;
+var CLIPBOARD_FEATURE = 2;
+var SHAREDOBJECT_FEATURE = 3;
+var VIDEO_FEATURE = 4;
+var SOUND_FEATURE = 5;
+var NETCONNECTION_FEATURE = 6;
 var create = Object.create;
 var defineProperty = Object.defineProperty;
 var keys = Object.keys;
 var isArray = Array.isArray;
 var fromCharCode = String.fromCharCode;
 var logE = Math.log;
 var max = Math.max;
 var min = Math.min;
@@ -3489,18 +3495,33 @@ var LoaderDefinition = function () {
                   type: 'frame'
                 };
                 break;
               }
             }
           },
           oncomplete: function (result) {
             commitData(result);
+            var stats;
+            if (typeof result.swfVersion === 'number') {
+              var bbox = result.bbox;
+              stats = {
+                topic: 'parseInfo',
+                parseTime: result.parseTime,
+                bytesTotal: result.bytesTotal,
+                swfVersion: result.swfVersion,
+                frameRate: result.frameRate,
+                width: (bbox.xMax - bbox.xMin) / 20,
+                height: (bbox.yMax - bbox.yMin) / 20,
+                isAvm2: !(!result.fileAttributes.doAbc)
+              };
+            }
             commitData({
-              command: 'complete'
+              command: 'complete',
+              stats: stats
             });
           }
         };
       }
       function parseBytes(bytes) {
         SWF.parse(bytes, createParsingContext());
       }
       if (isWorker || !flash.net.URLRequest.class.isInstanceOf(request)) {
@@ -3605,16 +3626,20 @@ var LoaderDefinition = function () {
               if (type === 'frameConstructed') {
                 frameConstructed.resolve();
                 avm2.systemDomain.onMessage.unregister('frameConstructed', waitForFrame);
               }
             });
             Promise.when(frameConstructed, this._lastPromise).then(function () {
               this.contentLoaderInfo._dispatchEvent('complete');
             }.bind(this));
+            var stats = data.stats;
+            if (stats) {
+              TelemetryService.reportTelemetry(stats);
+            }
             this._worker && this._worker.terminate();
             break;
           case 'empty':
             this._lastPromise = new Promise();
             this._lastPromise.resolve();
             break;
           case 'error':
             this.contentLoaderInfo._dispatchEvent('ioError', flash.events.IOErrorEvent);
@@ -6433,17 +6458,18 @@ CompressedPipe.prototype = {
       this.state.bitLength = stream.bitLength;
     }
     buffer.removeHead(stream.pos);
     this.target.push(output.data.subarray(lastAvailable, output.available), progressInfo);
   }
 };
 function BodyParser(swfVersion, length, options) {
   this.swf = {
-    swfVersion: swfVersion
+    swfVersion: swfVersion,
+    parseTime: 0
   };
   this.buffer = new HeadTailBuffer(32768);
   this.initialize = true;
   this.totalRead = 0;
   this.length = length;
   this.options = options;
 }
 BodyParser.prototype = {
@@ -6484,17 +6510,19 @@ BodyParser.prototype = {
     } else {
       buffer.push(data);
       stream = buffer.createStream();
     }
     if (progressInfo) {
       swf.bytesLoaded = progressInfo.bytesLoaded;
       swf.bytesTotal = progressInfo.bytesTotal;
     }
+    var readStartTime = Date.now();
     readTags(swf, stream, swfVersion, options.onprogress);
+    swf.parseTime += Date.now() - readStartTime;
     var read = stream.pos;
     buffer.removeHead(read);
     this.totalRead += read;
     if (this.totalRead >= this.length && options.oncomplete) {
       options.oncomplete(swf);
     }
   }
 };
--- a/browser/extensions/shumway/content/shumway.js
+++ b/browser/extensions/shumway/content/shumway.js
@@ -7663,17 +7663,18 @@ CompressedPipe.prototype = {
       this.state.bitLength = stream.bitLength;
     }
     buffer.removeHead(stream.pos);
     this.target.push(output.data.subarray(lastAvailable, output.available), progressInfo);
   }
 };
 function BodyParser(swfVersion, length, options) {
   this.swf = {
-    swfVersion: swfVersion
+    swfVersion: swfVersion,
+    parseTime: 0
   };
   this.buffer = new HeadTailBuffer(32768);
   this.initialize = true;
   this.totalRead = 0;
   this.length = length;
   this.options = options;
 }
 BodyParser.prototype = {
@@ -7714,17 +7715,19 @@ BodyParser.prototype = {
     } else {
       buffer.push(data);
       stream = buffer.createStream();
     }
     if (progressInfo) {
       swf.bytesLoaded = progressInfo.bytesLoaded;
       swf.bytesTotal = progressInfo.bytesTotal;
     }
+    var readStartTime = Date.now();
     readTags(swf, stream, swfVersion, options.onprogress);
+    swf.parseTime += Date.now() - readStartTime;
     var read = stream.pos;
     buffer.removeHead(read);
     this.totalRead += read;
     if (this.totalRead >= this.length && options.oncomplete) {
       options.oncomplete(swf);
     }
   }
 };
@@ -9267,16 +9270,21 @@ function interpretActions(actionsData, s
       }
     } catch (e) {
       if (!AVM1_ERRORS_IGNORED && !currentContext.isTryCatchListening || e instanceof AS2CriticalError) {
         throw e;
       }
       if (e instanceof AS2Error) {
         throw e;
       }
+      var AVM1_ERROR_TYPE = 1;
+      TelemetryService.reportTelemetry({
+        topic: 'error',
+        error: AVM1_ERROR_TYPE
+      });
       stream.position = nextPosition;
       if (stackItemsExpected > 0) {
         while (stackItemsExpected--) {
           stack.push(undefined);
         }
       }
       if (!recoveringFromError) {
         if (currentContext.errorsIgnored++ >= MAX_AVM1_ERRORS_LIMIT) {
@@ -39168,16 +39176,22 @@ QuadTree.prototype._subdivide = function
   var midX = this.x + halfWidth;
   var midY = this.y + halfHeight;
   var level = this.level + 1;
   this.nodes[0] = new QuadTree(midX, this.y, halfWidth, halfHeight, level);
   this.nodes[1] = new QuadTree(this.x, this.y, halfWidth, halfHeight, level);
   this.nodes[2] = new QuadTree(this.x, midY, halfWidth, halfHeight, level);
   this.nodes[3] = new QuadTree(midX, midY, halfWidth, halfHeight, level);
 };
+var EXTERNAL_INTERFACE_FEATURE = 1;
+var CLIPBOARD_FEATURE = 2;
+var SHAREDOBJECT_FEATURE = 3;
+var VIDEO_FEATURE = 4;
+var SOUND_FEATURE = 5;
+var NETCONNECTION_FEATURE = 6;
 {
   var BitmapDefinition = function () {
       function setBitmapData(value) {
         if (this._bitmapData) {
           this._bitmapData._changeNotificationTarget = null;
         }
         this._bitmapData = value;
         if (this._bitmapData) {
@@ -41336,18 +41350,33 @@ var LoaderDefinition = function () {
                   type: 'frame'
                 };
                 break;
               }
             }
           },
           oncomplete: function (result) {
             commitData(result);
+            var stats;
+            if (typeof result.swfVersion === 'number') {
+              var bbox = result.bbox;
+              stats = {
+                topic: 'parseInfo',
+                parseTime: result.parseTime,
+                bytesTotal: result.bytesTotal,
+                swfVersion: result.swfVersion,
+                frameRate: result.frameRate,
+                width: (bbox.xMax - bbox.xMin) / 20,
+                height: (bbox.yMax - bbox.yMin) / 20,
+                isAvm2: !(!result.fileAttributes.doAbc)
+              };
+            }
             commitData({
-              command: 'complete'
+              command: 'complete',
+              stats: stats
             });
           }
         };
       }
       function parseBytes(bytes) {
         SWF.parse(bytes, createParsingContext());
       }
       if (isWorker || !flash.net.URLRequest.class.isInstanceOf(request)) {
@@ -41452,16 +41481,20 @@ var LoaderDefinition = function () {
               if (type === 'frameConstructed') {
                 frameConstructed.resolve();
                 avm2.systemDomain.onMessage.unregister('frameConstructed', waitForFrame);
               }
             });
             Promise.when(frameConstructed, this._lastPromise).then(function () {
               this.contentLoaderInfo._dispatchEvent('complete');
             }.bind(this));
+            var stats = data.stats;
+            if (stats) {
+              TelemetryService.reportTelemetry(stats);
+            }
             this._worker && this._worker.terminate();
             break;
           case 'empty':
             this._lastPromise = new Promise();
             this._lastPromise.resolve();
             break;
           case 'error':
             this.contentLoaderInfo._dispatchEvent('ioError', flash.events.IOErrorEvent);
@@ -42562,16 +42595,21 @@ var MovieClipDefinition = function () {
           }
           if (frame in this._frameScripts) {
             var scripts = this._frameScripts[frame];
             try {
               for (var i = 0, n = scripts.length; i < n; i++) {
                 scripts[i].call(this);
               }
             } catch (e) {
+              var AVM2_ERROR_TYPE = 2;
+              TelemetryService.reportTelemetry({
+                topic: 'error',
+                error: AVM2_ERROR_TYPE
+              });
               if (false) {
                 console.error('error ' + e + ', stack: \n' + e.stack);
               }
               this.stop();
               throw e;
             }
           }
         },
@@ -44454,16 +44492,20 @@ var TimerEventDefinition = function () {
         initialize: function () {
         },
         __glue__: {
           native: {
             static: {
               _initJS: function _initJS() {
                 if (initialized)
                   return;
+                TelemetryService.reportTelemetry({
+                  topic: 'feature',
+                  feature: EXTERNAL_INTERFACE_FEATURE
+                });
                 initialized = true;
                 FirefoxCom.initJS(callIn);
               },
               _getPropNames: function _getPropNames(obj) {
                 var keys = [];
                 forEachPublicProperty(obj, function (key) {
                   keys.push(key);
                 });
@@ -45448,16 +45490,20 @@ var SoundDefinition = function () {
               soundData.mimeType = s.packaged.mimeType;
             }
             var _this = this;
             getAudioDescription(soundData, function (description) {
               _this._length = description.duration;
             });
             this._soundData = soundData;
           }
+          TelemetryService.reportTelemetry({
+            topic: 'feature',
+            feature: SOUND_FEATURE
+          });
         },
         close: function close() {
           somewhatImplemented('Sound.close');
         },
         extract: function extract(target, length, startPosition) {
           notImplemented('Sound.extract');
         },
         _load: function _load(request, checkPolicyFile, bufferTime) {
@@ -46203,16 +46249,20 @@ var StageVideoDefinition = function () {
           }
         }
       }
     };
   }.call(this);
 var VideoDefinition = function () {
     var def = {
         initialize: function initialize() {
+          TelemetryService.reportTelemetry({
+            topic: 'feature',
+            feature: VIDEO_FEATURE
+          });
         },
         attachNetStream: function (netStream) {
           this._netStream = netStream;
           netStream._videoReady.then(function (element) {
             this._element = element;
             netStream._videoMetadataReady.then(function (url) {
               this._element.width = this._videoWidth = this._element.videoWidth;
               this._element.height = this._videoHeight = this._element.videoHeight;
@@ -46360,16 +46410,20 @@ var VideoDefinition = function () {
         }
       };
     }.call(this);
 }
 var NetConnectionDefinition = function () {
     return {
       __class__: 'flash.net.NetConnection',
       initialize: function () {
+        TelemetryService.reportTelemetry({
+          topic: 'feature',
+          feature: NETCONNECTION_FEATURE
+        });
       },
       _invoke: function (index, args) {
         var simulated = false, result;
         switch (index) {
         case 2:
           simulated = true;
           break;
         }
@@ -46976,16 +47030,20 @@ var SharedObjectDefinition = function ()
     }
     return {
       __class__: 'flash.net.SharedObject',
       initialize: function () {
         this._data = {};
         this._objectEncoding = _defaultObjectEncoding;
         this._data[Multiname.getPublicQualifiedName('levelCompleted')] = 32;
         this._data[Multiname.getPublicQualifiedName('completeLevels')] = 32;
+        TelemetryService.reportTelemetry({
+          topic: 'feature',
+          feature: SHAREDOBJECT_FEATURE
+        });
       },
       __glue__: {
         native: {
           static: {
             deleteAll: function deleteAll(url) {
               notImplemented('SharedObject.deleteAll');
             },
             getDiskUsage: function getDiskUsage(url) {
@@ -47617,16 +47675,20 @@ var SystemDefinition = function () {
       __class__: 'flash.system.System',
       initialize: function () {
       },
       __glue__: {
         native: {
           static: {
             setClipboard: function setClipboard(string) {
               FirefoxCom.request('setClipboard', string);
+              TelemetryService.reportTelemetry({
+                topic: 'feature',
+                feature: CLIPBOARD_FEATURE
+              });
             },
             pause: function pause() {
               notImplemented('System.pause');
             },
             resume: function resume() {
               notImplemented('System.resume');
             },
             exit: function exit(code) {
--- a/browser/extensions/shumway/content/version.txt
+++ b/browser/extensions/shumway/content/version.txt
@@ -1,1 +1,1 @@
-0.7.352
+0.7.354
--- a/browser/extensions/shumway/content/web/avm-sandbox.js
+++ b/browser/extensions/shumway/content/web/avm-sandbox.js
@@ -154,16 +154,22 @@ window.addEventListener("message", funct
       var session = FileLoadingService.sessions[args.sessionId];
       if (session) {
         session.notify(args);
       }
       break;
   }
 }, true);
 
+var TelemetryService = {
+  reportTelemetry: function (data) {
+    FirefoxCom.request('reportTelemetry', data, null);
+  }
+};
+
 var FileLoadingService = {
   get baseUrl() { return movieUrl; },
   nextSessionId: 1, // 0 - is reserved
   sessions: [],
   createSession: function () {
     var sessionId = this.nextSessionId++;
     return this.sessions[sessionId] = {
       open: function (request) {
@@ -242,16 +248,18 @@ function parseSwf(url, movieParams, obje
 
 var pauseExecution = false;
 var initializeFrameControl = true;
 function frame(e) {
   if (initializeFrameControl) {
     // marking that movie is started
     document.body.classList.add("started");
 
+    TelemetryService.reportTelemetry({topic: "firstFrame"});
+
     // skipping frame 0
     initializeFrameControl = false;
     return;
   }
   if (pauseExecution) {
     e.cancel = true;
   }
 }