// vim: ts=2 sw=2 expandtab cindent
//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL
//

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;

const CLASS_ID = Components.ID("{2fd8e108-f21b-11db-8314-0800200c9a66}");
const CONTRACT_ID = "@flock.com/profiler;1";
const CATEGORY_COMPONENT_NAME = "Flock Profiler Component";

const GRAPH_HOST = "dippy.hq.flock.com";
const CREATEGRAPH_URL = "http://%host%/ffgraphs.xml";
const OBSERVATION_URL = "http://%host%/ffgraphs/%graphname%/ffmeasurements.xml";

const PREF_PROFILER = "flock.profiler.enabled";
const PREF_REPORTING = "flock.profiler.reporting.enabled";

const EVENT_FLOCKDATAREADY = "flock-data-ready";
const EVENT_XPCOMSHUTDOWN = "xpcom-shutdown";

var gGraphNames = [];

var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    gCompTK = Cc["@flock.com/singleton;1"]
                .getService(Ci.flockISingleton)
                .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
                .wrappedJSObject;
  }
  return gCompTK;
}


// ===============================================
// ========== BEGIN flockProfiler class ==========
// ===============================================

function flockProfiler()
{
  // Use a logger to spit out data to the console
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("profiler");

  // The System Info service will be used to get some performance data
  this._sysinfo = Cc["@flock.com/sysinfo;1"].getService(Ci.flockISystemInfo);
  var buildInfo = this._sysinfo.getBuildInfo().split(",");
  this._platform = buildInfo[0];
  this._firstRun = (buildInfo[1] == "1");
  this._startTime = (new Date()).getTime();
  //this._startTime = this._sysinfo.getStartTime();
  var sysinfoTime = this._sysinfo.getStartTime();
  this._logger.debug("start time reported by sysinfo:"+sysinfoTime);
  this._logger.info("construction time: "+this._startTime);

  // Register to observe application initialization and shutdown events
  this._obs = Cc["@mozilla.org/observer-service;1"]
    .getService(Ci.nsIObserverService);
  this.mInitialized = false;
  this._obs.addObserver(this, EVENT_FLOCKDATAREADY, false); // calls _init()
  this._obs.addObserver(this, EVENT_XPCOMSHUTDOWN, false);

  // This will be used to store open profiling events
  this._eventQueue = [];

  // These will be overridden in _init()
  this._profilingEnabled = false;
  this._reportingEnabled = false;

  // Use Component Toolkit to implement basic interfaces
  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIProfiler"
    ],
    shortName: "profiler",
    fullName: "Flock Profiler",
    CID: CLASS_ID,
    contractID: CONTRACT_ID
  };
}


// BEGIN nsIObserver
flockProfiler.prototype.observe =
function flockProfiler_observe(aSubject, aTopic, aData)
{
  switch (aTopic) {

    case EVENT_FLOCKDATAREADY:
    {
      this._obs.removeObserver(this, EVENT_FLOCKDATAREADY);
      this._init();
    }; break;

    case EVENT_XPCOMSHUTDOWN:
    {
      this._obs.removeObserver(this, EVENT_XPCOMSHUTDOWN);

      // Don't accept any more profiling calls or reporting after shutdown
      this._profilingEnabled = false;
      this._reportingEnabled = false;

      // Flush the event queue
      this._eventQueue = [];
    }; break;

  }
}
// END nsIObserver


// BEGIN flockIProfiler
flockProfiler.prototype.profile =
function flockProfiler_profile(aEventName, aMessage)
{
  this._init();
  if (!this._profilingEnabled) return;
  var inst = this;
  var pEvent = {
    seq: inst._getProfileSeq(),
    name: aEventName,
    clock: inst._getTimeSinceStart(),
    msg: aMessage,
    ready: true
  };
  pEvent.cpu = this._sysinfo.getCPU();
  pEvent.mem = this._sysinfo.getMem();
  this._eventQueue.push(pEvent);
  this._logEvent(pEvent);
}

flockProfiler.prototype.profileEventStart =
function flockProfiler_profileEventStart(aEventName)
{
  this._init();
  if (!this._profilingEnabled) return 0;
  var inst = this;
  var pEvent = {
    seq: inst._getProfileSeq(),
    name: aEventName,
    clock: inst._getTimeSinceStart()
  };
  //pEvent.cpu = this._sysinfo.getCPU();
  //pEvent.mem = this._sysinfo.getMem();
  this._eventQueue.push(pEvent);
  //this._logEvent(pEvent);
  return pEvent.seq;
}

flockProfiler.prototype.profileEventEnd =
function flockProfiler_profileEventEnd(aEventID, aMessage)
{
  if (!this._profilingEnabled) return;
  // Find the event start
  var pEvent = null;
  for (var i = this._eventQueue.length - 1; i >= 0; i--) {
    if (this._eventQueue[i].seq == aEventID) {
      pEvent = this._eventQueue[i];
      break;
    }
  }
  if (!pEvent) return;
  pEvent.duration = this._getTimeSinceStart() - pEvent.clock;
  pEvent.cpu = this._sysinfo.getCPU();
  pEvent.mem = this._sysinfo.getMem();
  pEvent.msg = aMessage;
  pEvent.ready = true;
  this._logEvent(pEvent);
}
// END flockIProfiler


flockProfiler.prototype._init =
function flockProfiler__init()
{
  if (this.mInitialized) return;

  // Check prefs to see if we should be enabled
  var prefSvc = Cc["@mozilla.org/preferences-service;1"]
    .getService(Ci.nsIPrefBranch);
  this._profilingEnabled = ( prefSvc.getPrefType(PREF_PROFILER) &&
                             prefSvc.getBoolPref(PREF_PROFILER) );
  this._reportingEnabled = ( prefSvc.getPrefType(PREF_REPORTING) &&
                             prefSvc.getBoolPref(PREF_REPORTING) );
  
  // Kick off a timer for processing the event queue
  if (this._profilingEnabled && this._reportingEnabled) {
    var inst = this;
    var timerCB = { notify: function (aTimer) { inst._processEventQueue(); } };
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    timer.initWithCallback(timerCB, 10 * 1000, Ci.nsITimer.TYPE_ONE_SHOT);
  }

  this.mInitialized = true;
}

flockProfiler.prototype._getProfileSeq =
function flockProfiler__getProfileSeq()
{
  return this._eventQueue.length + 1;
}

flockProfiler.prototype._logEvent =
function flockProfiler__logEvent(aEvent)
{
  var msg = "";
  var props = ["seq","name","clock","duration","cpu","mem","msg"];
  for (var p in props) {
    if (aEvent[props[p]] == undefined) continue;
    if (msg.length > 0) msg += " ";
    msg += props[p]+":"+aEvent[props[p]];
  }
  this._logger.info(msg);
}

flockProfiler.prototype._getTimeSinceStart =
function flockProfiler__getTimeSinceStart()
{
  var timeNow = (new Date()).getTime();
  var timeElapsed = timeNow - this._startTime;
  //dump("CDC profiling: timeNow: "+timeNow+" timeElapsed:"+timeElapsed+"\n");
  return timeElapsed;
}

flockProfiler.prototype._ensureGraphExists =
function flockProfiler__ensureGraphExists(aGraphName, aUnits, aListener)
{
  if (!this._reportingEnabled) return;
  if (gGraphNames[aGraphName] != undefined) {
    aListener.onSuccess();
  } else {
    var inst = this;
    var hr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
               .createInstance(Ci.nsIXMLHttpRequest);
    hr.onreadystatechange = function (aEvent) {
      if (hr.readyState == 4) {
        gGraphNames[aGraphName] = true;
        //inst._logger.info("created graph: "+aGraphName);
        //inst._logger.info("graph creation status: "+hr.status+" "+hr.statusText);
        //inst._logger.info("response text:"+hr.responseText);
        aListener.onSuccess();
      }
    };
    var graphURL = CREATEGRAPH_URL.replace("%host%", GRAPH_HOST)
    hr.open("POST", graphURL);
    var postBody = "ffgraph[name]="+aGraphName
                 + "&ffgraph[units]="+aUnits
                 + "&ffgraph[password]=flock";
    //inst._logger.info("POST to URL: "+graphURL);
    //inst._logger.info("POST body: "+postBody);
    hr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    hr.send(postBody);
  }
}

flockProfiler.prototype._submitObservations =
function flockProfiler__submitObservations(aProfilingEvent)
{
  if (!this._reportingEnabled) return;
  var obsTypes = ["clock","duration","cpu","mem"];
  var obsUnits = ["ms","ms","%","Mb"];
  for (var i in obsTypes) {
    var obsType = obsTypes[i];
    if (aProfilingEvent[obsType] != undefined) {
      switch (obsType) {
        case "cpu": case "mem":
          // Don't bother recording null observations
          if (aProfilingEvent[obsType] == 0) { continue; }
      }
      var graphName = aProfilingEvent.name+"%20"+obsType+"%20"+this._platform;
      if (this._firstRun) graphName += "%20FR";
      var inst = this;
      var graphCreationListener = {
        graphName: graphName,
        obsType: obsType,
        onSuccess: function () {
          if (!inst._reportingEnabled) return;
          var hr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                     .createInstance(Ci.nsIXMLHttpRequest);
          hr.onreadystatechange = function (aHTTPEvent) {
            if (hr.readyState == 4) {
              aProfilingEvent.sent = true;
            }
          };
          var obsURL = OBSERVATION_URL.replace("%host%", GRAPH_HOST)
                                      .replace("%graphname%", this.graphName);
          hr.open("POST", obsURL);
          var postBody = "ffmeasurement[data]="+aProfilingEvent[this.obsType]
                       + "&ffmeasurement[notes]="+escape(aProfilingEvent.msg);
          //inst._logger.info("POST to URL: "+obsURL);
          //inst._logger.info("POST body: "+postBody);
          hr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
          hr.setRequestHeader("Authorization", "Basic OmZsb2Nr"); // Base64(":flock")
          hr.send(postBody);
        }
      };
      this._ensureGraphExists(graphName, obsUnits[i], graphCreationListener);
    }
  }
}

flockProfiler.prototype._processEventQueue =
function flockProfiler__processEventQueue()
{
  if (!this._reportingEnabled) return;
  //this._logger.info("_processEventQueue()");
  while (this._eventQueue.length && this._eventQueue[0].sent) {
    this._eventQueue.shift();
  }
  var profilingEvent = null;
  var pos = 0;
  while (!profilingEvent && (pos < this._eventQueue.length)) {
    if (this._eventQueue[pos].ready) {
      profilingEvent = this._eventQueue[pos];
      break;
    }
    pos++;
  }
  if (profilingEvent) {
    this._submitObservations(profilingEvent);
  }
  var inst = this;
  var timerCB = { notify: function (aTimer) { inst._processEventQueue(); } };
  var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(timerCB, 5 * 1000, Ci.nsITimer.TYPE_ONE_SHOT);
}
// ========== END flockProfiler class ==========


// ================================================
// ========== BEGIN XPCOM Module support ==========
// ================================================

function createModule(aParams) {
  return {
    registerSelf: function (aCompMgr, aFileSpec, aLocation, aType) {
      aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        aParams.contractID, aFileSpec,
                                        aLocation, aType );
      var catMgr = Cc["@mozilla.org/categorymanager;1"]
        .getService(Ci.nsICategoryManager);
      catMgr.addCategoryEntry( "flock-startup", aParams.componentName,
                               "service,"+aParams.contractID, true, true );
      if (!aParams.categories) { aParams.categories = []; }
      for (var i = 0; i < aParams.categories.length; i++) {
        var cat = aParams.categories[i];
        catMgr.addCategoryEntry( cat.category, cat.entry,
                                 cat.value, true, true );
      }
    },
    getClassObject: function (aCompMgr, aCID, aIID) {
      if (!aCID.equals(aParams.CID)) { throw Cr.NS_ERROR_NO_INTERFACE; }
      if (!aIID.equals(Ci.nsIFactory)) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }
      return { // Factory
        createInstance: function (aOuter, aIID) {
          if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; }
          var comp = new aParams.componentClass();
          if (aParams.implementationFunc) { aParams.implementationFunc(comp); }
          return comp.QueryInterface(aIID);
        }
      };
    },
    canUnload: function (aCompMgr) { return true; }
  };
}

// NS Module entrypoint
function NSGetModule(aCompMgr, aFileSpec) {
  return createModule({
    componentClass: flockProfiler,
    CID: CLASS_ID,
    contractID: CONTRACT_ID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function (aComp) { getCompTK().addAllInterfaces(aComp); }
  });
}

// ========== END XPCOM Module support ==========
