-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve popup performance #1487
base: master
Are you sure you want to change the base?
Conversation
343925d
to
bfae211
Compare
I tried it and it seems a little better with 96 dictionaries enabled. |
bfae211
to
229163d
Compare
Tried the latest changes and it feels like a dramatic improvement. Great work! |
Same issue with loading dict collections or importing single dicts on the PR. (No response/animations after selecting a file). Loading recommended dicts still works however. I was able to test the PR by loading my dict collection with the main dev branch first and then copying over the files from this PR. The performance boost is significant for me with 35 imported dictionaries. |
Yeah there's almost certainly a problem, I forget that the importer also runs some parts of this DB code in a worker and I changed some of the DB code to behave differently based on whether it's executing in a worker or not and neglected to check the behavior of the import code. I'll take a look. Thanks for testing! |
Thanks, is that the entry for 矜持? |
yep |
Seems like images break after a certain point. After restarting my browser pictures work, scan a bit and they dissapear again. |
Can you guys open the service worker, as well as offscreen inspector tabs from the extension details page, and let me know if there are errors in either of those consoles? |
no console errors present in either. |
How about immediately after restarting the extension and doing the first lookup? It must be breaking somewhere 🤔 |
Didn't see any console errors while it was broken, and now after restarting I can't reproduce the issue. |
Just to leave a record, I've been using d3f1317 on Edge for the past few weeks and haven't had any issues. Although I haven't tried dictionary collection import which 4890 reported was broken. |
…erization, because that needs to be on the main thread...)
f4a139c
to
80e21c5
Compare
80e21c5
to
9b58a5e
Compare
3753f95
to
704b6e9
Compare
704b6e9
to
b3de480
Compare
CodSpeed Performance ReportMerging #1487 will improve performances by 42.01%Comparing Summary
Benchmarks breakdown
|
Dumb questions about this PR because I haven't really looked at it until today
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw you also have some leftover console.log
and commented out code that needs cleaning up. Very cool stuff I'm seeing here and excited to try to get this out the door.
I'm not done with my review I'm just doing a hard cutoff for bed without being completely blocked on me
); | ||
|
||
// when we are not a worker ourselves, create a worker which is basically just a wrapper around this class, which we can use to offload some functions to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "not a worker" equivalent to saying "main thread" or "main process"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, not exactly. On Chrome, we expect this to be running in the offscreen context, and in Firefox we expect it to be in the background page context. In theory it could also run in the SW context in Chrome though we don't do that currently since SW is short-lived and we want to keep the DB open longer. These contexts are neither threads or processes, as threads and processes are implementation details of the browser. (e.g., in Chrome each extension runs in its own process, and I'm not sure if SW and offscreen are in different processes, while on Firefox all extensions run in a single process, but any extension resources which are loaded as a child of a non-extension resource seems to be in the normal webpage process).
So "not a worker" is the most technically accurate way to capture all the possible situations here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to educate myself via ChatGPT. lmk if this explanation of the difference between offscreen, background, and sw context is wrong
https://chatgpt.com/share/67627dc8-294c-8002-a0a6-bb1344b88a74
// performance.mark('drawMedia:findMultiBulk:end'); | ||
// performance.measure('drawMedia:findMultiBulk', 'drawMedia:findMultiBulk:start', 'drawMedia:findMultiBulk:end'); | ||
|
||
// move all svgs to front to have a hotter loop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "hotter loop" mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CPUs do stuff like branch prediction and caching, so if you can do the same sort of processing all at the same time instead of interleaving different types of processing, you can get some performance benefits since the same branch gets executed over and over, and all necessary information is in your CPU caches. It's basically getting the benefits of batching at the micro scale.
* | ||
* # On application startup | ||
* application: create a new MessageChannel, bind event listeners to one of the ports, and send the other port to the bridge | ||
* ↓↓<"connectToBackend1" via SharedWorker.port.postMessage>↓↓ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connectToBackend1
and connectToBackend2
seem like two different logic
connectToBackend1
is an API call that responds to a message from the application
connectToBackend2
is logic on the backend to store the port as state
Am I understanding this correctly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm, that's not how I would describe it. Both of the messages serve just to pass a MessagePort from the application to the backend. The shared worker in the middle does nothing except just pass along the port. The backend stores the port.
/** @type {import('shared-worker').ApiHandler<'connectToBackend1'>} */ | ||
_onConnectToBackend1(_params, _interlocutorPort, ports) { | ||
if (this._backendPort !== null) { | ||
this._backendPort.postMessage(void 0, [ports[0]]); // connectToBackend2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of void 0
, could we use an event type or some other data to indicate it's a connectToBackend2
message? Right now connectToBackend2
's existence is only implied via comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally was doing that, but basically either you choose the fully typesafe API style of doing things like I did everywhere else, which requires a ton of extra code (like maybe 50~100 lines), but it seemed like overkill when there is only a single type of message which gets sent over this channel which has no (normal) parameters, so I decided to simplify it, since I can't imagine this ever getting more message types in the future (since the whole purpose of it is to establish a channel that you can use to send properly typed messages of any kind that you want). If you think it's too confusing I guess we can introduce the full API boilerplate, but we have a lot of different APIs due to this PR and I think introducing yet another one might be bad unless we figure out some better way to name them to make it clearer what's going on.
Yes, that's what it refers to. The performance enhancements improve general performance because almost all entries from recently created Yomitan dictionaries involve a high number of images, due to many dictionaries using tons of tiny SVGs for all sorts of different things (pitch accent markers, numbers, symbols, section tags, etc).
One CSS change (
Mmm, certainly
and then the rest as-needed in whatever order you want. |
…ance-canvas # Conflicts: # ext/js/display/display.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cinderella moment ending my review at my local midnight. Mostly did a high level pass through.
// which we can use to postMessage with the backend. | ||
// This can only be done in the extension context (aka iframe within popup), | ||
// not in the content script context. | ||
const backendPort = (!('serviceWorker' in navigator)) && window.location.protocol === new URL(import.meta.url).protocol ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const backendPort = (!('serviceWorker' in navigator)) && window.location.protocol === new URL(import.meta.url).protocol ? | |
const isFirefox = (!('serviceWorker' in navigator)) && window.location.protocol === new URL(import.meta.url).protocol; | |
const backendPort = isFirefox ? |
Maybe pull out the check into a labeled variable for readability? I'm not super following what the logic check is doing except by relying on the comment above. Is that what this check does?
@@ -190,10 +190,35 @@ export class Application extends EventDispatcher { | |||
* @param {(application: Application) => (Promise<void>)} mainFunction | |||
*/ | |||
static async main(waitForDom, mainFunction) { | |||
/** @type {MessagePort | null} */ | |||
// If this is Firefox, we don't have a service worker and can't postMessage, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"we" here refers to the application a.k.a the iframe within the popup?
can't postMessage
How is it that we postMessage to the sharedWorkerBridge but not directly to the backend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In terminology, is my understanding correct?
"application context" refers to the iframe that popups during scan and this file contains the relevant logic
"content script context" is the code that runs on each web page irrespective of whether a scan occurs.
const sharedWorkerBridge = new SharedWorker(new URL('comm/shared-worker-bridge.js', import.meta.url), {type: 'module'}); | ||
const backendChannel = new MessageChannel(); | ||
sharedWorkerBridge.port.postMessage({action: 'connectToBackend1'}, [backendChannel.port1]); | ||
sharedWorkerBridge.port.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the understanding here that the application and backend have now established a connection and this sharedWorkerBridge is no longer needed?
const api = new API(webExtension); | ||
|
||
const mediaDrawingWorkerToBackendChannel = new MessageChannel(); | ||
const mediaDrawingWorker = window.location.protocol === new URL(import.meta.url).protocol ? new Worker(new URL('display/media-drawing-worker.js', import.meta.url), {type: 'module'}) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const mediaDrawingWorker = window.location.protocol === new URL(import.meta.url).protocol ? new Worker(new URL('display/media-drawing-worker.js', import.meta.url), {type: 'module'}) : null;
Similarly pull out window.location.protocol === new URL(import.meta.url).protocol
as a variable to make it clear what this check is doing
case 'connectToDatabaseWorker': | ||
void this._dictionaryDatabase?.connectToDatabaseWorker(event.ports[0]); | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a default
case for logging?
@@ -0,0 +1,32 @@ | |||
/* | |||
* Copyright (C) 2023-2024 Yomitan Authors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Copyright (C) 2023-2024 Yomitan Authors | |
* Copyright (C) 2024 Yomitan Authors |
); | ||
|
||
// when we are not a worker ourselves, create a worker which is basically just a wrapper around this class, which we can use to offload some functions to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to educate myself via ChatGPT. lmk if this explanation of the difference between offscreen, background, and sw context is wrong
https://chatgpt.com/share/67627dc8-294c-8002-a0a6-bb1344b88a74
export class MediaDrawingWorker { | ||
constructor() { | ||
/** @type {number} */ | ||
this._generation = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "generation" mean in the context of Yomitan?
if (sizeUnits === 'em' && (hasPreferredWidth || hasPreferredHeight)) { | ||
image.style.width = `${usedWidth}em`; | ||
image.style.height = `${usedWidth * invAspectRatio}em`; | ||
image.width = usedWidth * 14 * 2 * window.devicePixelRatio; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
14 * 2
magic numbers?
Quite a bit of popup CPU usage comes from the following things:
The first is resolved easily by introducing
content-visibility: auto
, at the downside of the scrollbar being less accurate but I think it's the right tradeoff.The second is resolved by the rest of the PR, which is insane and probably impossible to understand.
Changes to media loading
Previous to this PR, media requests were sent one-by-one from the popup to the backend, which would generate a data URL that would be sent back to the popup and inserted in an image tag.
I considered another approach, which batches all the image requests for a given entry, prepares canvas elements, and sends OffscreenCanvases to the backend, which then fetches the unique set of images for that entry and draws the appropriate ones onto the appropriate canvases.
This helps quite a bit since we can avoid creating Blobs and Data URIs, and also avoid duplicate requests to IndexedDB. IndexedDB requests are very slow here.
However, Firefox can't do cross-process transfers of OffscreenCanvases, so I settled on an architecture of having a worker within the popup which does drawing onto the OffscreenCanvases, and the backend worker transfers ArrayBuffers to the popup worker for drawing (because Firefox can do cross-process transfers of those).
Changes to communications
Unfortunately, implementing the above was not so straightforward, as we primarily use
chrome.runtime.sendMessage
which does not allow for transferring objects and serializes everything, making the postMessage transfers impossible.So, I introduced a couple new communication channels:
postMessage
to the active SW (on Chrome), or establishes a MessageChannel using a temporary SharedWorker andpostMessages
over that (on Firefox)postMessage
over that channel to the offscreen. The offscreen watches for SW changes and reregisters so the SW will always have a channel for it.This builds a full
postMessage
channel all the way from the popup to the backend (offscreen in case of Chrome), which allows for building further MessageChannels between popup and various backend workers (dictionary worker in the case of this PR). And withpostMessage
comes the ability to transfer things, which is critical for performance.Potential improvements
drawMedia
. In the future we should probably consider some more generic architecture, where we have pools of dictionary workers, which can handle whatever dictionary-related work we want, and nothing DB-related is done in the main backend thread