Skip to content
This repository has been archived by the owner on May 25, 2021. It is now read-only.

Commit

Permalink
Major performance improvment by bypassing the sequence of ports when …
Browse files Browse the repository at this point in the history
…sending tree data (#592)

* Relatively minor performance fixes:
 * Revert the recursion change I made to the serializer as it performs way worse than the old implementation
 * Do not expand large arrays by default in the State view because it
   takes a long time to render them
(Note: There is a big performance fix coming around message transmission but this is not it)

* Major performance improvement by sending component data through an
alternate channel instead of the sequence of ports that require 4
serializations and deserializations for each message

* Up the delta threshold

* Remove debug statements

* Remove fallthrough switch statement (Igor's comment)
  • Loading branch information
clbond committed Sep 6, 2016
1 parent 428824c commit db9310a
Show file tree
Hide file tree
Showing 27 changed files with 447 additions and 227 deletions.
189 changes: 137 additions & 52 deletions src/backend/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
MutableTree,
Node,
Path,
deserializePath,
} from '../tree';

import {createTreeFromElements} from '../tree/mutable-tree-factory';
Expand All @@ -15,39 +14,67 @@ import {
Message,
MessageFactory,
MessageType,
browserDispatch,
browserSubscribe,
} from '../communication';

import {send} from './indirect-connection';

import {
Route,
MainRoute,
defineLookupOperation,
highlight,
parseRoutes,
} from './utils';

import {serialize} from '../utils';

import {MessageQueue} from '../structures';

import {SimpleOptions} from '../options';

declare const ng;
declare const getAllAngularRootElements: () => Element[];
declare const treeRenderOptions: SimpleOptions;

/// For tree deltas that contain more changes than {@link deltaThreshold},
/// we simply send the entire tree again instead of trying to patch it
/// since it will be faster than trying to apply hundreds or thousands of
/// changes to an existing tree.
const deltaThreshold = 512;

/// For large messages, we do not send them through the normal pipe (which
/// is backend > content script > backround channel > frontend), we add them
/// to this buffer and then send a {@link MessageType.Push} message that
/// tells the frontend to read messages directly from this queue itself.
/// This allows us to prevent very large messages containing tree data from
/// being serialized and deserialized four times. Using this mechanism, they
/// are serialized and deserialized a total of one times.
const messageBuffer = new MessageQueue<Message<any>>();

/// NOTE(cbond): We collect roots from all applications (mulit-app support)
let previousTree: MutableTree;

let previousCount: number;

const updateTree = (roots: Array<DebugElement>) => {
const showElements = treeRenderOptions.showElements;

const newTree = createTreeFromElements(roots, showElements);
const {tree, count} = createTreeFromElements(roots, showElements);

if (previousTree == null || Math.abs(previousCount - count) > deltaThreshold) {
messageBuffer.enqueue(MessageFactory.completeTree(tree));
}
else {
messageBuffer.enqueue(MessageFactory.treeDiff(previousTree.diff(tree)));
}

/// Send a message through the normal channels to indicate to the frontend
/// that messages are waiting for it in {@link messageBuffer}
send<void, void>(MessageFactory.push());

send<void, any>(
previousTree
? MessageFactory.treeDiff(previousTree.diff(newTree))
: MessageFactory.completeTree(newTree));
previousTree = tree;

previousTree = newTree;
previousCount = count;
};

const update = () => {
Expand All @@ -69,60 +96,63 @@ const bind = (root: DebugElement) => {

getAllAngularRootElements().forEach(root => bind(ng.probe(root)));

browserSubscribe(
(message: Message<any>) => {
switch (message.messageType) {
case MessageType.Initialize:
// Update our tree settings closure
Object.assign(treeRenderOptions, message.content);
const messageHandler = (message: Message<any>) => {
switch (message.messageType) {
case MessageType.Initialize:
// Update our tree settings closure
Object.assign(treeRenderOptions, message.content);

// Clear out existing tree representation and start over
previousTree = null;
// Clear out existing tree representation and start over
previousTree = null;

// Load the complete component tree
subject.next(void 0);
// Load the complete component tree
subject.next(void 0);

return true;
return true;

case MessageType.SelectComponent:
return tryWrap(() => {
const path: Path = message.content.path;
case MessageType.SelectComponent:
return tryWrap(() => {
const path: Path = message.content.path;

const node = previousTree.traverse(path);
const node = previousTree.traverse(path);

this.consoleReference(node);
this.consoleReference(node);

// For component selection events, we respond with component instance
// properties for the selected node. If we had to serialize the
// properties of each node on the tree that would be a performance
// killer, so we only send the componentInstance values for the
// node that has been selected.
if (message.content.requestInstance) {
return getComponentInstance(previousTree, node);
}
});
// For component selection events, we respond with component instance
// properties for the selected node. If we had to serialize the
// properties of each node on the tree that would be a performance
// killer, so we only send the componentInstance values for the
// node that has been selected.
if (message.content.requestInstance) {
return getComponentInstance(previousTree, node);
}
});

case MessageType.UpdateProperty:
return tryWrap(() => updateProperty(previousTree,
message.content.path,
message.content.newValue));
case MessageType.UpdateProperty:
return tryWrap(() => updateProperty(previousTree,
message.content.path,
message.content.newValue));

case MessageType.EmitValue:
return tryWrap(() => emitValue(previousTree,
message.content.path,
message.content.value));
case MessageType.EmitValue:
return tryWrap(() => emitValue(previousTree,
message.content.path,
message.content.value));

case MessageType.RouterTree:
return tryWrap(() => routerTree());
case MessageType.RouterTree:
return tryWrap(() => routerTree());

case MessageType.Highlight:
const nodes = message.content.nodes
.map(id => previousTree.search(id));
case MessageType.Highlight:
if (previousTree == null) {
return;
}
return tryWrap(() => {
highlight(message.content.nodes.map(id => previousTree.lookup(id)));
});
}
return undefined;
};

return tryWrap(() => highlight(nodes));
}
return undefined;
});
browserSubscribe(messageHandler);

// We do not store component instance properties on the node itself because
// we do not want to have to serialize them across backend-frontend boundaries.
Expand Down Expand Up @@ -226,4 +256,59 @@ export const tryWrap = (fn: Function) => {
}
};

defineLookupOperation(() => previousTree);
/// We need to define some operations that are accessible from the global scope so that
/// the frontend can invoke them using {@link inspectedWindow.eval}. But we try to do it
/// in a safe way and ensure that we do not overwrite any existing properties or functions
/// that share the same names. If we do encounter such things we throw an exception and
/// complain about it instead of continuing with bootstrapping.
export const defineWindowOperations = <T>(target, classImpl: T) => {
for (const key of Object.keys(classImpl)) {
if (target[key] != null) {
throw new Error(`A window function or object named ${key} would be overwritten`);
}
}

Object.assign(target, classImpl);
};

export class WindowOperations {
/// Note that the ID is a serialized path, and the first element in that path is the
/// index of the application that the node belongs to. So even though we have this
/// global lookup operation for things like 'inspect' and 'view source', it will find
/// the correct node even if multiple applications are instantiated on the same page.
nodeFromPath(id: string): Element {
if (previousTree == null) {
throw new Error('No tree exists');
}

const node = previousTree.lookup(id);
if (node == null) {
console.error(`Cannot find element associated with node ${id}`);
return null;
}
return node.nativeElement();
}

/// Post a response to a message from the frontend and dispatch it through normal channels
response<T>(response: Message<T>) {
browserDispatch(response);
}

/// Run the message handler and return the result immediately instead of posting a response
handleImmediate<T>(message: Message<T>) {
const result = messageHandler(message);
if (result) {
return serialize(result);
}
return null;
}

/// Read all messages in the buffer and remove them
readMessageQueue(): Array<Message<any>> {
return messageBuffer.dequeue();
}
}

const windowOperationsImpl = new WindowOperations();

defineWindowOperations(window || global || this, {inspectedApplication: windowOperationsImpl});
15 changes: 15 additions & 0 deletions src/backend/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ export const subscribe = (handler: MessageHandler) => {
};

export const send = <T>(message: Message<T>) => {
if (message.messageType === MessageType.CompleteTree ||
message.messageType === MessageType.TreeDif ||
message.messageType === MessageType.DispatchWrapper) {
/// These types of messages should never be sent through this mechanism. A DispatchWrapper
/// message is for communication between content-script and the backend and has no business
/// being sent to the frontend. Similarly, a message containing tree data should be sent
/// through the {@link MessageBuffer} mechanism in backend.ts instead of through this port.
/// Sending a message with the {@link send} function will cause that message to take a very
/// circuitous route and will be serialized and deserialized repeatedly. Therefore large
/// messages must be sent using the {@link MessageBuffer} mechanism in order to avoid major
/// performance bottlenecks and UI latency.
const description = MessageType[message.messageType];
throw new Error(`A ${description} message should never be posted through the communication port`);
}

return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message,
response => {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/indirect-connection.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
Message,
MessageFactory,
browserDispatch,
messageJumpContext,
browserSubscribeResponse,
} from '../communication';

export const send = <Response, T>(message: Message<T>): Promise<Response> => {
return new Promise((resolve, reject) => {
browserSubscribeResponse(message.messageId, response => resolve(response));
browserDispatch(MessageFactory.dispatchWrapper(message));
messageJumpContext(MessageFactory.dispatchWrapper(message));
});
};

1 change: 0 additions & 1 deletion src/backend/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './description';
export * from './highlighter';
export * from './lookup';
export * from './parse-router';
27 changes: 0 additions & 27 deletions src/backend/utils/lookup.ts

This file was deleted.

51 changes: 25 additions & 26 deletions src/channel/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,37 @@ const drainQueue = (port: chrome.runtime.Port, buffer: Array<any>) => {
};

chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
(message, sender, sendResponse) => {
if (message.messageType === MessageType.Initialize) {
sendResponse({ // note that this is separate from our message response system
extensionId: chrome.runtime.id
});
}

if (sender.tab) {
let sent = false;

const connection = connections.get(sender.tab.id);
if (connection) {
try {
connection.postMessage(message);
sent = true;
}
catch (err) {}
sendResponse({ // note that this is separate from our message response system
extensionId: chrome.runtime.id
});
}

if (sent === false) {
let queue = messageBuffer.get(sender.tab.id);
if (queue == null) {
queue = new Array<any>();
messageBuffer.set(sender.tab.id, queue);
if (sender.tab) {
let sent = false;

const connection = connections.get(sender.tab.id);
if (connection) {
try {
connection.postMessage(message);
sent = true;
}
catch (err) {}
}

queue.push(message);
if (sent === false) {
let queue = messageBuffer.get(sender.tab.id);
if (queue == null) {
queue = new Array<any>();
messageBuffer.set(sender.tab.id, queue);
}

queue.push(message);
}
}
}
return true;
});
return true;
});

chrome.runtime.onConnect.addListener(port => {
const listener = (message, sender) => {
Expand All @@ -87,5 +87,4 @@ chrome.runtime.onConnect.addListener(port => {
}
});
});

});
Loading

0 comments on commit db9310a

Please sign in to comment.