Bug 1390037 - create a connector for connect to chrome remote debugging protocol on netmonitor. r?honza,rickychien draft
authorLocke Chen <locke12456@gmail.com>
Thu, 24 Aug 2017 13:21:25 +0800
changeset 651879 72ab56cefa5341d14031c73f0b321c923c8b5824
parent 650428 5ef76f53d514f3a5d0b10d66d84066710843f22b
child 727895 ba1698582c12b66fac6789c5d39e980a92ee8e6d
push id75844
push userbmo:locke12456@gmail.com
push dateThu, 24 Aug 2017 05:21:44 +0000
reviewershonza, rickychien
bugs1390037
milestone57.0a1
Bug 1390037 - create a connector for connect to chrome remote debugging protocol on netmonitor. r?honza,rickychien MozReview-Commit-ID: DwEkykZPIG5
devtools/client/netmonitor/src/connector/chrome/bulk-loader.js
devtools/client/netmonitor/src/connector/chrome/events.js
devtools/client/netmonitor/src/connector/chrome/request.js
devtools/client/netmonitor/src/connector/chrome/response.js
devtools/client/netmonitor/src/connector/chrome/utils.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/bulk-loader.js
@@ -0,0 +1,128 @@
+/* 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/. */
+
+"use strict";
+
+let bulkLoader = undefined;
+
+let PriorityLevels = {Critical: 1, Major: 2, Normal: 3, None: 0};
+
+class Scheduler {
+  constructor() {
+    this.busy = false;
+    this.queue = [];
+  }
+
+  sync(task) {
+    this.queue.push(task);
+    if (!this.busy) {
+      return this.dequeue();
+    }
+    return null;
+  }
+
+  dequeue() {
+    let self = this;
+    this.busy = true;
+    let next = this.queue.shift();
+    if (next) {
+      next().then(
+        (resolve) => {
+          self.dequeue();
+        }, (reject) => {
+          self.dequeue();
+        });
+    } else {
+      this.busy = false;
+    }
+  }
+}
+// singleton class
+const getBulkLoader = () => {
+  const mappingPriority = (priority, options) => {
+    switch (priority) {
+      case PriorityLevels.Critical:
+        return options.Critical;
+      case PriorityLevels.Major:
+        return options.Major;
+      case PriorityLevels.Normal:
+        return options.Normal;
+      case PriorityLevels.None:
+      default:
+        break;
+    }
+    return options.None;
+  };
+
+  const getTimeoutMS = (priority) => {
+    const delay = {Critical: 3000, Major: 1000, Normal: 500, None: 100};
+    return mappingPriority(priority, delay);
+  };
+
+  const getDelayStartMS = (priority) => {
+    const delay = {Critical: 1, Major: 50, Normal: 100, None: 500};
+    return mappingPriority(priority, delay);
+  };
+
+  const LoaderPromise = (priority, callback) => {
+    return new Promise((resolve, reject) => {
+      const ms = getTimeoutMS(priority);
+      // Set up the real work
+      setTimeout(() => callback(resolve, reject), getDelayStartMS(priority));
+
+      // Set up the timeout
+      setTimeout(() => {
+        reject("Promise timed out after " + ms + " ms");
+      }, ms);
+    });
+  };
+
+    // TODO : recovery thread after all tasks finished.
+  class Thread {
+    constructor() {
+      this.scheduler = new Scheduler();
+    }
+
+    addTask(callback, priority) {
+      this.scheduler.sync(() => {
+        return LoaderPromise(
+          !priority ? PriorityLevels.None : priority,
+          (resolve, reject) => callback(resolve, reject)
+        );
+      });
+    }
+  }
+
+  class BulkLoader {
+    constructor() {
+      this.threads = new Map();
+      this.tasks = [];
+    }
+
+    add(id, callback, priority) {
+      let thread = this.threads.get(priority);
+      if (!this.threads.has(priority)) {
+        thread = new Thread();
+        this.threads.set(priority, thread);
+      }
+      this.tasks.push({id, priority, task: callback, isFinished: false});
+      return thread.addTask(callback, priority);
+    }
+
+    reset() {
+      this.threads.clear();
+    }
+  }
+
+  if (!bulkLoader) {
+    bulkLoader = new BulkLoader();
+  }
+
+  return bulkLoader;
+};
+
+module.exports = {
+  getBulkLoader,
+  PriorityLevels
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/events.js
@@ -0,0 +1,235 @@
+/* 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/. */
+
+"use strict";
+const {EVENTS} = require("../../constants");
+const {Payloads} = require("./utils");
+const {getBulkLoader, PriorityLevels} = require("./bulk-loader");
+
+class CDPConnector {
+  constructor() {
+    this.payloads = new Payloads();
+    this.onNetworkUpdate = this.onNetworkUpdate.bind(this);
+    this.onResponseReceived = this.onResponseReceived.bind(this);
+    this.onDataReceived = this.onDataReceived.bind(this);
+    this.onLoadingFinished = this.onLoadingFinished.bind(this);
+    this.onLoadingFailed = this.onLoadingFailed.bind(this);
+    this.update = this.update.bind(this);
+  }
+
+  setup(connection, actions) {
+    let {Network, Page} = connection;
+    this.Network = Network;
+    this.Page = Page;
+    this.actions = actions;
+    Network.requestWillBeSent(this.onNetworkUpdate);
+    Network.responseReceived(this.onResponseReceived);
+    Network.dataReceived(this.onDataReceived);
+    Network.loadingFinished(this.onLoadingFinished);
+    Network.loadingFailed(this.onLoadingFailed);
+    Network.enable();
+    Page.enable();
+  }
+
+  disconnect() {
+    this.Network.disable();
+    this.Page.disable();
+    this.payloads.clear();
+  }
+
+  async reset() {
+    return Promise.all([
+      this.Network.disable(),
+      this.Page.disable(),
+      this.payloads.clear(),
+      this.Network.enable(),
+      this.Page.enable()
+    ]);
+  }
+
+  willNavigate(event) {
+    // not support
+  }
+
+  onNetworkUpdate(params) {
+    let {requestId} = params;
+    let payload = this.payloads.add(requestId);
+    return payload.update(params).then(
+      ([request, header, postData]) => {
+        let bulkloader = getBulkLoader();
+        bulkloader.add(
+          requestId,
+          (resolve, reject) =>
+            this.addRequest(requestId, request).then(() => {
+              this.updateRequestHeader(requestId, header);
+              this.updatePostData(requestId, postData);
+              resolve();
+            })
+          , PriorityLevels.Critical);
+      });
+  }
+
+  onResponseReceived(params) {
+    let {requestId} = params;
+    let payload = this.payloads.get(requestId);
+    return payload.update(params).then(
+      ([request, header, postData, state, timings]) => {
+        let loader = getBulkLoader();
+        loader.add(
+          requestId,
+          (resolve) => {
+            this.updateResponseHeader(requestId, header);
+            this.updateResponseState(requestId, state);
+            this.updateResponseTiming(requestId, timings);
+            this.getResponseContent(params);
+            resolve();
+          }
+          , PriorityLevels.Major);
+      });
+  }
+
+  onDataReceived(params) {
+    let {requestId} = params;
+    let payload = this.payloads.get(requestId);
+    payload.update(params);
+  }
+
+  onLoadingFinished(params) {
+    let {requestId} = params;
+    let payload = this.payloads.get(requestId);
+    if (payload) {
+      payload.log("LoadingFinished", params);
+    }
+    // TODO: verify getCookie method.
+    //
+  }
+
+  updateRequestHeader(requestId, header) {
+    if (!header) {
+      return;
+    }
+    this.update(requestId, {
+      requestHeaders: header
+    }).then(() => {
+      window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, header);
+    });
+  }
+
+  updateResponseTiming(requestId, timings) {
+    if (!timings) {
+      return;
+    }
+    this.update(requestId, {
+      eventTimings: timings
+    }).then(() => {
+      window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, requestId);
+    });
+  }
+
+  updateResponseState(requestId, state) {
+    this.update(requestId, state).then(() => {
+      window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, requestId);
+    });
+  }
+
+  updateResponseHeader(requestId, header) {
+    if (!header) {
+      return;
+    }
+    this.update(requestId, {
+      responseHeaders: header
+    }).then(() => {
+      window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, header);
+    });
+  }
+
+  onLoadingFailed(params) {
+    let {requestId} = params;
+    let payload = this.payloads.get(requestId);
+    if (payload) {
+      payload.log("LoadingFailed", params);
+    }
+    // console.log(params.requestId);
+  }
+
+  async getResponseContent(params) {
+    let {requestId, response} = params;
+
+    return this.Network.getResponseBody({requestId}).then(
+      (content) => {
+        let payload = this.payloads.get(requestId);
+        return payload.update({requestId, response, content}).then(
+          ([request, header, postData, state, timings, responseContent]) => {
+            let loader = getBulkLoader();
+            loader.add(
+              requestId,
+              (resolve) => {
+                this.updateResponseContent(requestId, responseContent);
+                return resolve();
+              },
+              PriorityLevels.Normal
+            );
+          }
+        );
+      }
+    );
+  }
+
+  updateResponseContent(requestId, payload) {
+    if (!payload) {
+      return;
+    }
+    this.actions.updateRequest(requestId, payload, true).then(
+      () => {
+        window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, requestId);
+      }
+    );
+  }
+
+  updatePostData(requestId, postData) {
+    if (!postData) {
+      return;
+    }
+    this.update(requestId, {
+      requestPostData: postData,
+    }).then(() => {
+      window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, requestId);
+    });
+  }
+
+  async update(id, payload) {
+    return this.actions.updateRequest(id, payload, true);
+  }
+
+  async addRequest(id, data) {
+    let {
+      method,
+      url,
+      isXHR,
+      cause,
+      startedDateTime,
+      fromCache,
+      fromServiceWorker,
+    } = data;
+
+    this.actions.addRequest(
+      id,
+      {
+        startedMillis: startedDateTime,
+        method,
+        url,
+        isXHR,
+        cause,
+        fromCache,
+        fromServiceWorker,
+      },
+      true,
+    )
+      .then(() => window.emit(EVENTS.REQUEST_ADDED, id));
+  }
+}
+
+module.exports = {
+  CDPConnector
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/request.js
@@ -0,0 +1,107 @@
+/* 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/. */
+
+"use strict";
+
+function mappingCallFrames(callFrames) {
+  let stacktrace = [];
+  callFrames.forEach(
+    (frame) => {
+      let {
+        functionName,
+        scriptId,
+        url,
+        lineNumber,
+        columnNumber
+      } = frame;
+      let stack = {
+        scriptId,
+        filename: url,
+        lineNumber,
+        columnNumber,
+        functionName,
+      };
+      stacktrace.push(stack);
+    }
+  );
+  return stacktrace;
+}
+
+function Cause(initiator) {
+  let {url, type, stack} = initiator;
+  let {callFrames} = stack || {};
+  if (!stack || !callFrames.length) {
+    return undefined;
+  }
+  let cause = {
+    type: type,
+    loadingDocumentUri: url,
+    stacktrace: mappingCallFrames(callFrames)
+  };
+  return cause;
+}
+
+function Header(id, headers) {
+  let header = [];
+  let headersSize = 0;
+  Object.keys(headers).map((value) => {
+    header.push(
+      {
+        name: value,
+        value: headers[value],
+      }
+    );
+    headersSize += value.length + headers[value].length;
+  });
+
+  return {
+    from: id,
+    headers: header,
+    headersSize: headersSize,
+    rawHeaders: undefined,
+  };
+}
+function PostData(id, postData, header) {
+  let {headers, headersSize} = header;
+  let payload = {},
+    requestPostData = {
+      from: id, postDataDiscarded: false, postData: {}
+    };
+  if (postData) {
+    requestPostData.postData.text = postData;
+    payload.requestPostData = Object.assign({}, requestPostData);
+    payload.requestHeadersFromUploadStream = {headers, headersSize};
+  }
+  return payload;
+}
+
+/**
+ * Not support on current version.
+ * unstable method: Network.getCookies
+ * cause: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-getCookies
+ */
+function Cookie(id, Network) {
+  // TODO: verify
+}
+
+function Request(id, requestData) {
+  let {request, initiator, timestamp} = requestData;
+  let {url, method} = request;
+  let cause = !initiator ? undefined : Cause(initiator);
+  return {
+    method, url, cause,
+    isXHR: false, // TODO: verify
+    startedDateTime: timestamp,
+    fromCache: undefined,
+    fromServiceWorker: undefined
+  };
+}
+
+module.exports = {
+  Cause,
+  Cookie,
+  Header,
+  Request,
+  PostData
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/response.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+"use strict";
+
+const { formDataURI } = require("../../utils/request-utils");
+
+function ResponseInfo(id, response, content) {
+  let {
+    mimeType
+  } = response;
+  const {body, base64Encoded} = content;
+  return {
+    from: id,
+    content: {
+      mimeType: mimeType,
+      text: !body ? "" : body,
+      size: !body ? 0 : body.length,
+      encoding: base64Encoded ? "base64" : undefined
+    }
+  };
+}
+
+function ResponseContent(id, response, content) {
+  const {body, base64Encoded} = content;
+  let {mimeType, encodedDataLength} = response;
+  let responseContent = ResponseInfo(id, response, content);
+  let payload = Object.assign(
+    {
+      responseContent,
+      contentSize: !body ? 0 : body.length,
+      transferredSize: encodedDataLength, // TODO: verify
+      mimeType: mimeType
+    }, body);
+  if (mimeType.includes("image/")) {
+    payload.responseContentDataUri = formDataURI(mimeType, base64Encoded, response);
+  }
+  return payload;
+}
+
+/**
+ * Not support on current version.
+ * unstable method: Security
+ * cause: https://chromedevtools.github.io/devtools-protocol/tot/Security/
+ */
+function SecurityDetails(id, security) {
+  // TODO : verify
+
+  return {};
+}
+
+function Timings(id, timing) {
+  // TODO : implement
+  let {
+    dnsStart,
+    dnsEnd,
+    connectStart,
+    connectEnd,
+    sendStart,
+    sendEnd,
+    receiveHeadersEnd
+  } = timing;
+  let dns = parseInt(dnsEnd - dnsStart, 10);
+  let connect = parseInt(connectEnd - connectStart, 10);
+  let send = parseInt(sendEnd - sendStart, 10);
+  let total = parseInt(receiveHeadersEnd, 10);
+  return {
+    from: id,
+    timings: {
+      blocked: 0,
+      dns: dns,
+      connect: connect,
+      send: send,
+      wait: parseInt(receiveHeadersEnd - (send + connect + dns), 10),
+      receive: 0,
+    },
+    totalTime: total,
+  };
+}
+function State(response, headers) {
+  let { headersSize } = headers;
+  let {
+    status,
+    statusText,
+    remoteIPAddress,
+    remotePort
+  } = response;
+  return {
+    remoteAddress: remoteIPAddress,
+    remotePort,
+    status,
+    statusText,
+    headersSize
+  };
+}
+module.exports = {
+  State,
+  Timings,
+  ResponseContent,
+  SecurityDetails
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/utils.js
@@ -0,0 +1,132 @@
+/* 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/. */
+
+"use strict";
+
+const { Request, Header, PostData } = require("./request");
+const { State, ResponseContent, Timings} = require("./response");
+const { getBulkLoader } = require("./bulk-loader");
+
+class Payload {
+  constructor() {
+    this.payload = {};
+    this.update = this.update.bind(this);
+  }
+  async update(payload) {
+    let { request, response, requestId, timestamp,
+          content, dataLength, encodedDataLength } = payload;
+    let {
+      headers,
+      postData,
+      timing
+    } = (request ? request : response) || {};
+
+    const header = await this.mappingHeader(requestId, headers);
+
+    this.requestId = requestId;
+
+    this.updateTimestamp(timestamp);
+    let data = await this.mappingAll(
+      requestId,
+      {
+        payload, response, postData,
+        header, content, timing,
+        dataLength, encodedDataLength
+      }
+    );
+    return data;
+  }
+
+  log(reason, info) {
+    this.updatePayload({
+      type: reason,
+      log: info
+    });
+  }
+
+  updateTimestamp(timestamp) {
+    let {request} = this.payload;
+    this.updatePayload(
+      request ? { response: timestamp } : { request: timestamp }
+    );
+  }
+
+  updatePayload(data) {
+    this.payload = Object.assign({}, this.payload, data);
+  }
+
+  async mappingAll(requestId, data) {
+    let {payload, response, postData,
+         header, content, timing,
+         dataLength, encodedDataLength } = data;
+    let [requests, headers, post,
+         status, timings, responses]
+        = await Promise.all(
+          [
+            this.mappingRequest(requestId, payload),
+            header,
+            this.mappingRequestPostData(requestId, postData, header),
+            this.mappingResponseStatus(requestId, response, header),
+            this.mappingTiming(requestId, timing),
+            this.mappingResponseContent(requestId, response, content)
+          ]);
+    this.updatePayload({
+      requests, headers, post, status, timings, responses, dataLength, encodedDataLength
+    });
+    return [ requests, headers, post, status, timings, responses ];
+  }
+
+  async mappingTiming(requestId, timing) {
+    return !timing ? undefined : Timings(requestId, timing);
+  }
+
+  async mappingRequest(requestId, payload) {
+    let {request} = payload;
+    return !request ? undefined : Request(requestId, payload);
+  }
+
+  async mappingHeader(requestId, headers) {
+    return !headers ? undefined : Header(requestId, headers);
+  }
+
+  async mappingRequestPostData(requestId, postData, headers) {
+    return !postData ? undefined : PostData(requestId, postData, headers);
+  }
+
+  async mappingResponseStatus(requestId, response, header) {
+    return !response ? undefined : State(response, header);
+  }
+
+  async mappingResponseContent(requestId, response, content) {
+    return !response || !content ?
+      undefined : ResponseContent(requestId, response, content);
+  }
+}
+class Payloads {
+  constructor() {
+    this.payloads = new Map();
+  }
+
+  add(id) {
+    if (!this.payloads.has(id)) {
+      this.payloads.set(id, new Payload());
+    }
+    return this.payloads.get(id);
+  }
+
+  get(id) {
+    return this.payloads.has(id) ?
+      this.payloads.get(id) : undefined;
+  }
+
+  clear() {
+    this.payloads.clear();
+    let loader = getBulkLoader();
+    loader.reset();
+  }
+}
+
+module.exports = {
+  Payload, Payloads
+};