[IMP] devtools: patch app methods only when needed

This commit changes the moment when all methods that need patching
to handle events are actually patched: the complete method of RootFiber
is now patched at devtools startup (when the tab owl tab is opened) and
every other method is patched at first activation of the event recording
functionality. This makes sure that the devtools will have no impact on
performance whatsoever when they are not directly put in use.
This commit is contained in:
Julien Carion (juca)
2023-06-21 12:58:25 +02:00
committed by Géry Debongnie
parent 23c7d19ef0
commit 601a98e649
2 changed files with 65 additions and 97 deletions
@@ -410,12 +410,7 @@ export const store = reactive({
if (!scriptsLoaded) { if (!scriptsLoaded) {
await loadScripts(frame); await loadScripts(frame);
} }
evalInWindow( evalFunctionInWindow("initDevtools", [frame], frame);
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
store.devtoolsId
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
frame
);
if (!this.frameUrls.includes(frame)) { if (!this.frameUrls.includes(frame)) {
this.frameUrls = [...this.frameUrls, frame]; this.frameUrls = [...this.frameUrls, frame];
} }
@@ -635,7 +630,7 @@ init();
async function init() { async function init() {
store.devtoolsId = await getTabURL(); store.devtoolsId = await getTabURL();
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";"); evalFunctionInWindow("initDevtools", []);
await loadSettings(); await loadSettings();
@@ -677,7 +672,7 @@ browserInstance.runtime.onConnect.addListener((port) => {
if (msg.type === "Reload") { if (msg.type === "Reload") {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;"); store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) { if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";"); evalFunctionInWindow("initDevtools", []);
await store.resetData(); await store.resetData();
} }
} }
@@ -22,7 +22,6 @@
// Set to keep track of the frame on which this script is loaded // Set to keep track of the frame on which this script is loaded
this.frame = "top"; this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page // Allows to launch a message each time an iframe html element is added to the page
const self = this;
const iFrameObserver = new MutationObserver(function (mutationsList) { const iFrameObserver = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) { mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) { mutation.addedNodes.forEach(function (addedNode) {
@@ -45,12 +44,7 @@
}); });
}); });
iFrameObserver.observe(document.body, { subtree: true, childList: true }); iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods(); this.patchAppsSetMethods();
if (this.apps.size > 0) {
this.patchAppMethods();
}
this.recordEvents = false; this.recordEvents = false;
this.traceRenderings = false; this.traceRenderings = false;
this.traceSubscriptions = false; this.traceSubscriptions = false;
@@ -170,34 +164,40 @@
}; };
} }
initDevtools(frame = "top") {
if (!this.devtoolsInit) {
this.frame = frame;
const self = this;
// Flush the events batcher when a root render is completed
const original_Complete = self.RootFiber.prototype.complete;
self.RootFiber.prototype.complete = function () {
original_Complete.call(this, ...arguments);
const path = self.getComponentPath(this.node);
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
window.top.postMessage({
source: "owl-devtools",
type: "Complete",
data: path,
origin: { frame: self.frame },
});
if (self.recordEvents) {
window.top.postMessage({
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
});
self.eventsBatch = [];
}
};
this.devtoolsInit = true;
}
}
// Modify the methods of the apps set in order to send a message each time it is modified. // Modify the methods of the apps set in order to send a message each time it is modified.
patchAppsSetMethods() { patchAppsSetMethods() {
const originalAdd = this.apps.add; const originalAdd = this.apps.add;
const originalDelete = this.apps.delete; const originalDelete = this.apps.delete;
const self = this;
this.apps.add = function () { this.apps.add = function () {
originalAdd.call(this, ...arguments); originalAdd.call(this, ...arguments);
if (!self.destroyPatched) {
const newApp = arguments[0];
// It is not a given that apps have a root node at creation so we need to wait
if (newApp.root) {
self.patchDestroyMethod(newApp.root);
} else {
let root = null;
Object.defineProperty(newApp, "root", {
get() {
return root;
},
set(value) {
root = value;
if (!self.destroyPatched) {
self.patchDestroyMethod(root);
}
},
});
}
}
self.patchAppMethods();
window.top.postMessage({ window.top.postMessage({
source: "owl-devtools", source: "owl-devtools",
type: "RefreshApps", type: "RefreshApps",
@@ -212,53 +212,18 @@
}; };
} }
patchDestroyMethod(root) {
if (!this.destroyPatched) {
// Signals when a component is destroyed
const originalDestroy = root.constructor.prototype._destroy;
const self = this;
root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.destroyPatched = true;
}
}
// Modify methods of each app so that it triggers messages on each flush and component render // Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() { patchAppMethods() {
if (this.appsPatched) { let app;
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
if (!app.root) {
return; return;
} }
let app = this.apps.values().next().value;
const self = this; const self = this;
if (app.root) {
this.patchDestroyMethod(app.root);
} else {
const originalMount = app.constructor.prototype.mount;
app.constructor.prototype.mount = async function (...args) {
const result = await originalMount.call(this, ...args);
const root = this.root;
self.patchDestroyMethod(root);
app.constructor.prototype.mount = originalMount;
return result;
};
}
const originalFlush = app.scheduler.constructor.prototype.flush; const originalFlush = app.scheduler.constructor.prototype.flush;
let inFlush = false; let inFlush = false;
let _render = false; let _render = false;
@@ -361,28 +326,27 @@
_render = true; _render = true;
original_Render.call(this, ...arguments); original_Render.call(this, ...arguments);
}; };
// Flush the events batcher when a root render is completed // Signals when a component is destroyed
const original_Complete = self.RootFiber.prototype.complete; const originalDestroy = app.root.constructor.prototype._destroy;
self.RootFiber.prototype.complete = function () { app.root.constructor.prototype._destroy = function () {
original_Complete.call(this, ...arguments);
const path = self.getComponentPath(this.node);
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
window.top.postMessage({
source: "owl-devtools",
type: "Complete",
data: path,
origin: { frame: self.frame },
});
if (self.recordEvents) { if (self.recordEvents) {
window.top.postMessage({ const path = self.getComponentPath(this);
source: "owl-devtools", const event = {
type: "Event", type: "destroy",
data: self.eventsBatch, component: this.name,
}); key: this.parentKey,
self.eventsBatch = []; path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
} }
}; };
this.appsPatched = true;
} }
// patch reactivity system to activate subscription tracing // patch reactivity system to activate subscription tracing
@@ -446,9 +410,14 @@
} }
toggleTracing(value) { toggleTracing(value) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.traceRenderings = value; this.traceRenderings = value;
return this.traceRenderings; return this.traceRenderings;
} }
toggleSubscriptionTracing(value) { toggleSubscriptionTracing(value) {
if (value) { if (value) {
this.patchReactivity(); this.patchReactivity();
@@ -459,6 +428,10 @@
} }
// Enables/disables the recording of the render/destroy events based on value // Enables/disables the recording of the render/destroy events based on value
toggleEventsRecording(value, index) { toggleEventsRecording(value, index) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.recordEvents = value; this.recordEvents = value;
this.eventId = index; this.eventId = index;
return this.recordEvents; return this.recordEvents;