Bug 1182595 - Add common, e10s-friendly SPS Profiler scripts that Talos tests can use. r?jmaher draft
authorMike Conley <mconley@mozilla.com>
Tue, 15 Dec 2015 15:57:39 -0500
changeset 333172 c4936868c4af071cb7cc24c706247a0b8d9def3d
parent 332881 789a12291942763bc1e3a89f97e0b82dc1c9d00b
child 333173 b6ac01d2adabbf1437b0b5cc0bdea8ec29f1f066
push id11283
push usermconley@mozilla.com
push dateMon, 22 Feb 2016 22:35:40 +0000
reviewersjmaher
bugs1182595
milestone47.0a1
Bug 1182595 - Add common, e10s-friendly SPS Profiler scripts that Talos tests can use. r?jmaher This introduces TalosContentProfiler.js, which can be used within content, and TalosParentProfiler.js, which can be used inside the parent process. MozReview-Commit-ID: 4L7rRuNALOy
testing/talos/talos/talos-powers/chrome.manifest
testing/talos/talos/talos-powers/chrome/talos-powers-content.js
testing/talos/talos/talos-powers/components/TalosPowersService.js
testing/talos/talos/talos-powers/content/TalosContentProfiler.js
testing/talos/talos/talos-powers/content/TalosParentProfiler.js
--- a/testing/talos/talos/talos-powers/chrome.manifest
+++ b/testing/talos/talos/talos-powers/chrome.manifest
@@ -1,4 +1,5 @@
 content talos-powers chrome/
+content talos-powers-content content/ contentaccessible=yes
 component {f5d53443-d58d-4a2f-8df0-98525d4f91ad} components/TalosPowersService.js
 contract @mozilla.org/talos/talos-powers-service;1 {f5d53443-d58d-4a2f-8df0-98525d4f91ad}
 category profile-after-change TalosPowersService @mozilla.org/talos/talos-powers-service;1
--- a/testing/talos/talos/talos-powers/chrome/talos-powers-content.js
+++ b/testing/talos/talos/talos-powers/chrome/talos-powers-content.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var { interfaces: Ci } = Components;
+var { interfaces: Ci, utils: Cu } = Components;
 
 /**
  * Content that wants to quit the whole session should
  * fire the TalosQuitApplication custom event. This will
  * attempt to force-quit the browser.
  */
 addEventListener("TalosQuitApplication", event => {
   // If we're loaded in a low-priority background process, like
@@ -17,8 +17,29 @@ addEventListener("TalosQuitApplication",
   let priority = docShell.QueryInterface(Ci.nsIDocumentLoader)
                          .loadGroup
                          .QueryInterface(Ci.nsISupportsPriority)
                          .priority;
   if (priority != Ci.nsISupportsPriority.PRIORITY_LOWEST) {
     sendAsyncMessage("Talos:ForceQuit", event.detail);
   }
 });
+
+addEventListener("TalosContentProfilerCommand", (e) => {
+  let name = e.detail.name;
+  let data = e.detail.data;
+  sendAsyncMessage("TalosContentProfiler:Command", { name, data });
+});
+
+addMessageListener("TalosContentProfiler:Response", (msg) => {
+  let name = msg.data.name;
+  let data = msg.data.data;
+
+  let event = Cu.cloneInto({
+    bubbles: true,
+    detail: {
+      name: name,
+      data: data,
+    },
+  }, content);
+  content.dispatchEvent(
+    new content.CustomEvent("TalosContentProfilerResponse", event));
+});
\ No newline at end of file
--- a/testing/talos/talos/talos-powers/components/TalosPowersService.js
+++ b/testing/talos/talos/talos-powers/components/TalosPowersService.js
@@ -2,63 +2,204 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+  "resource://gre/modules/osfile.jsm");
 
 const FRAME_SCRIPT = "chrome://talos-powers/content/talos-powers-content.js";
 
-function TalosPowersService() {};
+function TalosPowersService() {
+  this.wrappedJSObject = this;
+};
 
 TalosPowersService.prototype = {
   classDescription: "Talos Powers",
   classID: Components.ID("{f5d53443-d58d-4a2f-8df0-98525d4f91ad}"),
   contractID: "@mozilla.org/talos/talos-powers-service;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   observe(subject, topic, data) {
     switch(topic) {
       case "profile-after-change":
         // Note that this observation is registered in the chrome.manifest
         // for this add-on.
         this.init();
         break;
-      case "sessionstore-windows-restored":
-        this.inject();
-        break;
       case "xpcom-shutdown":
         this.uninit();
         break;
     }
   },
 
   init() {
-    // We want to defer any kind of work until after sessionstore has
-    // finished in order not to skew sessionstore test numbers.
-    Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+    Services.mm.loadFrameScript(FRAME_SCRIPT, true);
+    Services.mm.addMessageListener("Talos:ForceQuit", this);
+    Services.mm.addMessageListener("TalosContentProfiler:Command", this);
     Services.obs.addObserver(this, "xpcom-shutdown", false);
   },
 
   uninit() {
-    Services.obs.removeObserver(this, "sessionstore-windows-restored", false);
     Services.obs.removeObserver(this, "xpcom-shutdown", false);
   },
 
-  inject() {
-    Services.mm.loadFrameScript(FRAME_SCRIPT, true);
-    Services.mm.addMessageListener("Talos:ForceQuit", this);
+  receiveMessage(message) {
+    switch(message.name) {
+      case "Talos:ForceQuit": {
+        this.forceQuit(message.data);
+        break;
+      }
+      case "TalosContentProfiler:Command": {
+        this.receiveProfileCommand(message);
+        break;
+      }
+    }
+  },
+
+  /**
+   * Enable the SPS profiler with some settings and then pause
+   * immediately.
+   *
+   * @param data (object)
+   *        A JavaScript object with the following properties:
+   *
+   *        entries (int):
+   *          The sampling buffer size in bytes.
+   *
+   *        interval (int):
+   *          The sampling interval in milliseconds.
+   *
+   *        threadsArray (array of strings):
+   *          The thread names to sample.
+   */
+  profilerBegin(data) {
+    Services.profiler
+            .StartProfiler(data.entries, data.interval,
+                           ["js", "leaf", "stackwalk", "threads"], 4,
+                           data.threadsArray, data.threadsArray.length);
+
+    Services.profiler.PauseSampling();
+  },
+
+  /**
+   * Assuming the Profiler is running, dumps the Profile from all sampled
+   * processes and threads to the disk. The Profiler will be stopped once
+   * the profiles have been dumped. This method returns a Promise that
+   * will resolve once this has occurred.
+   *
+   * @param profileFile (string)
+   *        The name of the file to write to.
+   *
+   * @returns Promise
+   */
+  profilerFinish(profileFile) {
+    return new Promise((resolve, reject) => {
+      Services.profiler.getProfileDataAsync().then((profile) => {
+        let encoder = new TextEncoder();
+        let array = encoder.encode(JSON.stringify(profile));
+
+        OS.File.writeAtomic(profileFile, array, {
+          tmpPath: profileFile + ".tmp",
+        }).then(() => {
+          Services.profiler.StopProfiler();
+          resolve();
+        });
+      }, (error) => {
+        Cu.reportError("Failed to gather profile: " + error);
+        // FIXME: We should probably send a message down to the
+        // child which causes it to reject the waiting Promise.
+        reject();
+      });
+    });
   },
 
-  receiveMessage(message) {
-    if (message.name == "Talos:ForceQuit") {
-      this.forceQuit(message.data);
+  /**
+   * Pauses the Profiler, optionally setting a parent process marker before
+   * doing so.
+   *
+   * @param marker (string, optional)
+   *        A marker to set before pausing.
+   */
+  profilerPause(marker=null) {
+    if (marker) {
+      Services.profiler.AddMarker(marker);
+    }
+
+    Services.profiler.PauseSampling();
+  },
+
+  /**
+   * Resumes a pausedProfiler, optionally setting a parent process marker
+   * after doing so.
+   *
+   * @param marker (string, optional)
+   *        A marker to set after resuming.
+   */
+  profilerResume(marker=null) {
+    Services.profiler.ResumeSampling();
+
+    if (marker) {
+      Services.profiler.AddMarker(marker);
+    }
+  },
+
+  /**
+   * Adds a marker to the Profile in the parent process.
+   */
+  profilerMarker(marker) {
+    Services.profiler.AddMarker(marker);
+  },
+
+  receiveProfileCommand(message) {
+    const ACK_NAME = "TalosContentProfiler:Response";
+    let mm = message.target.messageManager;
+    let name = message.data.name;
+    let data = message.data.data;
+
+    switch(name) {
+      case "Profiler:Begin": {
+        this.profilerBegin(data);
+        // profilerBegin will cause the parent to send an async message to any
+        // child processes to start profiling. Because messages are serviced
+        // in order, we know that by the time that the child services the
+        // ACK message, that the profiler has started in its process.
+        mm.sendAsyncMessage(ACK_NAME, { name });
+        break;
+      }
+
+      case "Profiler:Finish": {
+        // The test is done. Dump the profile.
+        let profileFile = data.profileFile;
+        this.profilerFinish(data.profileFile).then(() => {
+          mm.sendAsyncMessage(ACK_NAME, { name });
+        });
+        break;
+      }
+
+      case "Profiler:Pause": {
+        this.profilerPause(data.marker);
+        mm.sendAsyncMessage(ACK_NAME, { name });
+        break;
+      }
+
+      case "Profiler:Resume": {
+        this.profilerResume(data.marker);
+        mm.sendAsyncMessage(ACK_NAME, { name });
+        break;
+      }
+
+      case "Profiler:Marker": {
+        this.profilerMarker(data.marker);
+        mm.sendAsyncMessage(ACK_NAME, { name });
+      }
     }
   },
 
   forceQuit(messageData) {
     if (messageData && messageData.waitForSafeBrowsing) {
       let SafeBrowsing = Cu.import("resource://gre/modules/SafeBrowsing.jsm", {}).SafeBrowsing;
 
       let whenDone = () => {
new file mode 100644
--- /dev/null
+++ b/testing/talos/talos/talos-powers/content/TalosContentProfiler.js
@@ -0,0 +1,242 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This utility script is for instrumenting your Talos test for
+ * performance profiles while running within content. If your test
+ * is running in the parent process, you should use
+ * TalosParentProfiler.js instead to avoid the messaging overhead.
+ */
+
+var TalosContentProfiler;
+
+(function() {
+
+  // Whether or not this TalosContentProfiler object has had initFromObject
+  // or initFromURLQueryParams called on it. Any functions that will send
+  // events to the parent to change the behaviour of the SPS Profiler
+  // should only be called after calling either initFromObject or
+  // initFromURLQueryParams.
+  var initted = false;
+
+  // The subtest name that beginTest() was called with.
+  var currentTest = "unknown";
+
+  // Profiler settings.
+  var interval, entries, threadsArray, profileDir;
+
+  try {
+    // Outside of talos, this throws a security exception which no-op this file.
+    // (It's not required nor allowed for addons since Firefox 17)
+    // It's used inside talos from non-privileged pages (like during tscroll),
+    // and it works because talos disables all/most security measures.
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+  } catch (e) {}
+
+  Components.utils.import("resource://gre/modules/Services.jsm");
+
+  /**
+   * Emits a TalosContentProfiler prefixed event and then returns a Promise
+   * that resolves once a corresponding acknowledgement event is
+   * dispatched on our document.
+   *
+   * @param name
+   *        The name of the event that will be TalosContentProfiler prefixed and
+   *        eventually sent to the parent.
+   * @param data (optional)
+   *        The data that will be sent to the parent.
+   * @returns Promise
+   *        Resolves when a corresponding acknowledgement event is dispatched
+   *        on this document.
+   */
+  function sendEventAndWait(name, data={}) {
+    return new Promise((resolve) => {
+      var event = new CustomEvent("TalosContentProfilerCommand", {
+        bubbles: true,
+        detail: {
+          name: name,
+          data: data,
+        }
+      });
+      document.dispatchEvent(event);
+
+      addEventListener("TalosContentProfilerResponse", function onResponse(event) {
+        if (event.detail.name != name) {
+          return;
+        }
+
+        removeEventListener("TalosContentProfilerResponse", onResponse);
+
+        resolve(event.detail.data);
+      });
+    });
+  }
+
+  /**
+   * Parses an url query string into a JS object.
+   *
+   * @param locationSearch (string)
+   *        The location string to parse.
+   * @returns Object
+   *        The GET params from the location string as
+   *        key-value pairs in the Object.
+   */
+  function searchToObject(locationSearch) {
+    var pairs = locationSearch.substring(1).split("&");
+    var result = {};
+
+    for (var i in pairs) {
+      if (pairs[i] !== "") {
+        var pair = pairs[i].split("=");
+        result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+      }
+    }
+
+    return result;
+  }
+
+  TalosContentProfiler = {
+    /**
+     * Initialize the profiler using profiler settings supplied in a JS object.
+     *
+     * @param obj (object)
+     *   The following properties on the object are respected:
+     *     sps_profile_interval (int)
+     *     sps_profile_entries (int)
+     *     sps_profile_threads (string, comma separated list of threads to filter with)
+     *     sps_profile_dir (string)
+     */
+    initFromObject(obj={}) {
+      if (!initted) {
+        if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
+            ("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
+            ("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
+            ("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
+          interval = obj.sps_profile_interval;
+          entries = obj.sps_profile_entries;
+          threadsArray = obj.sps_profile_threads.split(",");
+          profileDir = obj.sps_profile_dir;
+          initted = true;
+        } else {
+          console.error("Profiler could not init with object: " + JSON.stringify(obj));
+        }
+      }
+    },
+
+    /**
+     * Initialize the profiler using a string from a location string.
+     *
+     * @param locationSearch (string)
+     *        The location string to initialize with.
+     */
+    initFromURLQueryParams(locationSearch) {
+      this.initFromObject(searchToObject(locationSearch));
+    },
+
+    /**
+     * A Talos test is about to start. This will return a Promise that
+     * resolves once the Profiler has been initialized. Note that the
+     * SPS profiler will be paused immediately after starting and that
+     * resume() should be called in order to collect samples.
+     *
+     * @param testName (string)
+     *        The name of the test to use in Profiler markers.
+     * @returns Promise
+     *        Resolves once the SPS profiler has been initialized and paused.
+     */
+    beginTest(testName) {
+      if (initted) {
+        currentTest = testName;
+        return sendEventAndWait("Profiler:Begin", {
+          interval,
+          entries,
+          threadsArray,
+        });
+      } else {
+        var msg = "You should not call beginTest without having first " +
+                  "initted the Profiler"
+        console.error(msg);
+        return Promise.reject(msg);
+      }
+    },
+
+    /**
+     * A Talos test has finished. This will stop the SPS profiler from sampling,
+     * and return a Promise that resolves once the Profiler has finished dumping
+     * the multi-process profile to disk.
+     *
+     * @returns Promise
+     *          Resolves once the profile has been dumped to disk. The test should
+     *          not try to quit the browser until this has resolved.
+     */
+    finishTest() {
+      if (initted) {
+        let profileFile = profileDir + "/" + currentTest + ".sps";
+        return sendEventAndWait("Profiler:Finish", { profileFile });
+      } else {
+        var msg = "You should not call finishTest without having first " +
+                  "initted the Profiler";
+        console.error(msg);
+        return Promise.reject(msg);
+      }
+    },
+
+    /**
+     * A start-up test has finished. Callers don't need to run beginTest or
+     * finishTest, but should pause the sampler as soon as possible, and call
+     * this function to dump the profile.
+     *
+     * @returns Promise
+     *          Resolves once the profile has been dumped to disk. The test should
+     *          not try to quit the browser until this has resolved.
+     */
+    finishStartupProfiling() {
+      let profileFile = profileDir + "/startup.sps";
+      return sendEventAndWait("Profiler:Finish", { profileFile });
+    },
+
+    /**
+     * Resumes the SPS profiler sampler. Can also simultaneously set a marker.
+     *
+     * @returns Promise
+     *          Resolves once the SPS profiler has resumed.
+     */
+    resume(marker="") {
+      return sendEventAndWait("Profiler:Resume", { marker });
+    },
+
+    /**
+     * Pauses the SPS profiler sampler. Can also simultaneously set a marker.
+     *
+     * @returns Promise
+     *          Resolves once the SPS profiler has paused.
+     */
+    pause(marker="") {
+      return sendEventAndWait("Profiler:Pause", { marker });
+    },
+
+    /**
+     * Adds a marker to the profile.
+     *
+     * @returns Promise
+     *          Resolves once the marker has been set.
+     */
+    mark(marker) {
+      // If marker is omitted, just use the test name
+      if (!marker) {
+        marker = currentTest;
+      }
+
+      return sendEventAndWait("Profiler:Marker", { marker });
+    },
+
+    /**
+     * Add a marker to the profile on the content process samples.
+     * This occurs synchronously.
+     */
+    contentMarker(marker) {
+      Services.profiler.AddMarker(marker);
+    },
+  };
+})();
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/talos/talos/talos-powers/content/TalosParentProfiler.js
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This utility script is for instrumenting your Talos test for
+ * performance profiles while running within the parent process.
+ * Almost all of the functions that this script exposes to the
+ * SPS Profiler are synchronous, except for finishTest, since that
+ * involves requesting the profiles from any content processes and
+ * then writing to disk.
+ *
+ * If your test is running in the content process, you should use
+ * TalosContentProfiler.js instead.
+ */
+
+var TalosParentProfiler;
+
+(function() {
+
+  // Whether or not this TalosContentProfiler object has had initFromObject
+  // or initFromURLQueryParams called on it. Any functions that change the
+  // state of the SPS Profiler should only be called after calling either
+  // initFromObject or initFromURLQueryParams.
+  let initted = false;
+
+  // The subtest name that beginTest() was called with.
+  let currentTest = "unknown";
+
+  // Profiler settings.
+  let interval, entries, threadsArray, profileDir;
+
+  Components.utils.import("resource://gre/modules/Services.jsm");
+  Components.utils.import("resource://gre/modules/Console.jsm");
+
+  // Use a bit of XPCOM hackery to get at the Talos Powers service
+  // implementation...
+  let TalosPowers =
+    Components.classes["@mozilla.org/talos/talos-powers-service;1"]
+              .getService(Components.interfaces.nsISupports)
+              .wrappedJSObject;
+
+  /**
+   * Parses an url query string into a JS object.
+   *
+   * @param locationSearch (string)
+   *        The location string to parse.
+   * @returns Object
+   *        The GET params from the location string as
+   *        key-value pairs in the Object.
+   */
+  function searchToObject(locationSearch) {
+    let pairs = locationSearch.substring(1).split("&");
+    let result = {};
+
+    for (let i in pairs) {
+      if (pairs[i] !== "") {
+        let pair = pairs[i].split("=");
+        result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+      }
+    }
+
+    return result;
+  }
+
+  TalosParentProfiler = {
+    /**
+     * Initialize the profiler using profiler settings supplied in a JS object.
+     *
+     * @param obj (object)
+     *   The following properties on the object are respected:
+     *     sps_profile_interval (int)
+     *     sps_profile_entries (int)
+     *     sps_profile_threads (string, comma separated list of threads to filter with)
+     *     sps_profile_dir (string)
+     */
+    initFromObject(obj={}) {
+      if (!initted) {
+        if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
+            ("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
+            ("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
+            ("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
+          interval = obj.sps_profile_interval;
+          entries = obj.sps_profile_entries;
+          threadsArray = obj.sps_profile_threads.split(",");
+          profileDir = obj.sps_profile_dir;
+          initted = true;
+        } else {
+          console.error("Profiler could not init with object: " + JSON.stringify(obj));
+        }
+      }
+    },
+
+    /**
+     * Initialize the profiler using a string from a location string.
+     *
+     * @param locationSearch (string)
+     *        The location string to initialize with.
+     */
+    initFromURLQueryParams(locationSearch) {
+      this.initFromObject(searchToObject(locationSearch));
+    },
+
+    /**
+     * A Talos test is about to start. Note that the SPS profiler will be
+     * paused immediately after starting and that resume() should be called
+     * in order to collect samples.
+     *
+     * @param testName (string)
+     *        The name of the test to use in Profiler markers.
+     */
+    beginTest(testName) {
+      if (initted) {
+        currentTest = testName;
+        TalosPowers.profilerBegin({ entries, interval, threadsArray });
+      } else {
+        let msg = "You should not call beginTest without having first " +
+                  "initted the Profiler"
+        console.error(msg);
+      }
+    },
+
+    /**
+     * A Talos test has finished. This will stop the SPS profiler from sampling,
+     * and return a Promise that resolves once the Profiler has finished dumping
+     * the multi-process profile to disk.
+     *
+     * @returns Promise
+     *          Resolves once the profile has been dumped to disk. The test should
+     *          not try to quit the browser until this has resolved.
+     */
+    finishTest() {
+      if (initted) {
+        let profileFile = profileDir + "/" + currentTest + ".sps";
+        return TalosPowers.profilerFinish(profileFile);
+      } else {
+        let msg = "You should not call finishTest without having first " +
+                  "initted the Profiler";
+        console.error(msg);
+        return Promise.reject(msg);
+      }
+    },
+
+    /**
+     * A start-up test has finished. Callers don't need to run beginTest or
+     * finishTest, but should pause the sampler as soon as possible, and call
+     * this function to dump the profile.
+     *
+     * @returns Promise
+     *          Resolves once the profile has been dumped to disk. The test should
+     *          not try to quit the browser until this has resolved.
+     */
+    finishStartupProfiling() {
+      let profileFile = profileDir + "/startup.sps";
+      return TalosPowers.profilerFinish(profileFile);
+    },
+
+    /**
+     * Resumes the SPS profiler sampler. Can also simultaneously set a marker.
+     *
+     * @returns Promise
+     *          Resolves once the SPS profiler has resumed.
+     */
+    resume(marker="") {
+      TalosPowers.profilerResume(marker);
+    },
+
+    /**
+     * Pauses the SPS profiler sampler. Can also simultaneously set a marker.
+     *
+     * @returns Promise
+     *          Resolves once the SPS profiler has paused.
+     */
+    pause(marker="") {
+      TalosPowers.profilerPause(marker);
+    },
+
+    /**
+     * Adds a marker to the profile.
+     *
+     * @returns Promise
+     *          Resolves once the marker has been set.
+     */
+    mark(marker) {
+      // If marker is omitted, just use the test name
+      if (!marker) {
+        marker = currentTest;
+      }
+
+      TalosPowers.profilerMarker(marker);
+    },
+  };
+})();
\ No newline at end of file