Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Change browser extension to use a single MutationObserver #2909

Merged
merged 18 commits into from
Apr 1, 2019

Conversation

felixfbecker
Copy link
Contributor

@felixfbecker felixfbecker commented Mar 22, 2019

Refactors browser extension to use a single MutationObserver whose events are passed as an Observable to functions, instead of only injecting things once and never removing them.
Code views are then detected inside added and removed nodes. When a node with a code view got removed, its state gets properly cleaned up.

It fixes a bunch of bugs/wrong assumptions I found - for example, switchMap was used on the codeViews Observable which could race to randomly drop code views, and the selections and view components code assumed only the latest code view emitted was relevant. Hoverify was never unsubscribed from.

Also does a few renames that made things less confusing for me, e.g. CodeView -> CodeViewSpec, CodeView.codeView -> CodeView.codeViewElement

Also removes a hacky MutationObserver workaround in favor of the real fix sourcegraph/codeintellify#100

I've not touched unit tests yet. The functions that take MutationRecords can now be unit tested.
Also not touched other code hosts than GitHub yet.

Fixes #2162
Fixes https://github.com/sourcegraph/sourcegraph/issues/2958
Fixes #3103
Fixes #955

Tested on

  • GitHub
  • GitHub + Refined GitHub
  • GitHub Enterprise
  • Bitbucket Server
  • Phabricator
  • Gitlab

Copy link
Contributor

@lguychard lguychard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome start 👍

Also not touched other code hosts than GitHub yet.

It seems like this change would be code host-agnostic. What do you have in mind?

The functions that take MutationRecords can now be unit tested.

🎉

? null
: originalDOM.getCodeElementFromTarget(target),
}
selectionsChanges.subscribe(selections => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right, it assumes that all visible view components have the same selections.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wasn't that the behaviour before too? The selections are a property of the code host, so I don't see a way to distinguish which selections belong to which code view

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour wasn't exactly the same before - because we only considered the latest code view as relevant, when selections changed they were only updated on the latest code view.

This explicitly sets the same selections on all code views, which is knowingly wrong and I'd like us to avoid that. PTAL https://github.com/sourcegraph/sourcegraph/pull/2937 which offers a solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But only considering the "latest" code view was not correct, since there really isn't such a thing as the "latest" code view (the order in which code views are emitted is random as it depends on MutationObserver and the API requests to fetch contents). I think the previous code would still have the previous selections on the non-"latest" code view so selections did not just apply to the latest

// TODO optimize
// Maybe getMount() would have to be replaced by a function that determines if a new mount was added from a given mutation record

// injectCommandPalette({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to command palette injection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just commented out things I hadn't vetted for idempotency yet

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vetted for idempotency

IMO, idempotency should be provided by the implementor of CodeHost, not the user. This will allow for more logic to stay out of this file. It also seems like the correct place since the code host knows what the DOM looks like but this file should be ignorant of anything code host specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijsnow could you clarify what you mean by that?

)
// .pipe(emitWhenIntersecting(250))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your thinking on doing away with emitWithIntersecting()? I assume it was put in place for performance reasons when we used to scrape document contents from the DOM, but now that we're fetching the file contents instead, we're probably OK without.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just commented it out to exclude it as a possible source of why code views would not be detected properly. So far I've not noticed a perf problem. Maybe @ijsnow can comment on the reasoning behind it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emitting only when code views are visible or about to be visible was ported over from the old way of handling code hosts. It was added by Kingy in that implementation. It was indeed for performance problems. While the need for it may be reduced, I don't think it would hurt to keep it in.

@@ -156,16 +157,18 @@ export interface CodeHost {
*/
getOverlayMount?: () => HTMLElement | null

// Code views and code view resolver form a union
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is probably a "note to self", but it's probably worth being reworded & moved to the docstrings of codeViewSpecs and codeViewSpecResolver:

The set of code views tracked on a page is the union of all code views found using codeViewSpecs and codeViewResolver (or similar)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am dissatisfied by the redundancy of codeViewSpecs and codeViewSpecResolver. It causes redundant code to handle both, but every codeViewSpec could be expressed by a codeViewResolver. However, multiple codeViewSpecs are allowed, but only one codeViewSpecResolver. I would like to unify them somehow. We could also think about the usefullness of specifying a selector vs simply having (container: Element) => CodeViewWithoutSelector[]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also think about the usefullness of specifying a selector vs simply having (container: Element) => CodeViewWithoutSelector[]

Why would the resolve function return an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijsnow that would not be a resolve function, it would just return all the code views including their elements given a container.

lguychard referenced this pull request Mar 22, 2019
Having selections as a property of `CodeHost` is a problem, because [it is incorrect to assume that all code views on the page have the same selections](https://github.com/sourcegraph/sourcegraph/pull/2909#discussion_r268261523).

This adds `getSelections` and `observeSelections as optional properties of `CodeViewSpec`, and changes the code to make sure that when selections change, we don't update _all visible view components_ with the same spec of selection.

This assumes that only adding `getSelections()` and `observeSelections()` to single file code views is "good enough", which is still an imperfect assumption, but at least avoids having code that explicitly sets the same selections on a bunch of code views.
@felixfbecker
Copy link
Contributor Author

It seems like this change would be code host-agnostic. What do you have in mind?

There is a compile error in client/browser/src/libs/phabricator/extension.tsx since injectCodeIntelligence() is called but now needs the mutations Observable. I am not sure why this error is only in the Phab code. Maybe this is dead code?

@@ -156,16 +157,18 @@ export interface CodeHost {
*/
getOverlayMount?: () => HTMLElement | null

// Code views and code view resolver form a union
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also think about the usefullness of specifying a selector vs simply having (container: Element) => CodeViewWithoutSelector[]

Why would the resolve function return an array?

// TODO optimize
// Maybe getMount() would have to be replaced by a function that determines if a new mount was added from a given mutation record

// injectCommandPalette({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vetted for idempotency

IMO, idempotency should be provided by the implementor of CodeHost, not the user. This will allow for more logic to stay out of this file. It also seems like the correct place since the code host knows what the DOM looks like but this file should be ignorant of anything code host specific.

)
// .pipe(emitWhenIntersecting(250))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emitting only when code views are visible or about to be visible was ported over from the old way of handling code hosts. It was added by Kingy in that implementation. It was indeed for performance problems. While the need for it may be reduced, I don't think it would hurt to keep it in.

async function injectModules(): Promise<void> {
// This is added so that the browser extension doesn't
// interfere with the native Phabricator integration.
// TODO is this racy?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not intended to be racy. The logic is that the browser extension code runs after all other code is parsed/ran in the document. The native integration runs via a script tag added to the document by Phabricator. However, I just realized that we don't account for async code. I believe that it generally works but we should add the global class name that we look for to an element in the function that loads the phabricator bundle.

@felixfbecker felixfbecker changed the title Refactor browser extension to use a single MutationObserver Change browser extension to use a single MutationObserver Mar 31, 2019
@felixfbecker felixfbecker added this to the 3.4 milestone Mar 31, 2019
@codecov
Copy link

codecov bot commented Apr 1, 2019

Codecov Report

Merging #2909 into master will decrease coverage by 3.69%.
The diff coverage is 61.4%.

Impacted Files Coverage Δ
...wser/src/libs/code_intelligence/external_links.tsx 97.22% <ø> (-0.28%) ⬇️
client/browser/src/libs/phabricator/util.tsx 7.56% <ø> (-0.64%) ⬇️
.../browser/src/shared/components/CodeViewToolbar.tsx 27.02% <0%> (ø) ⬆️
...lient/browser/src/libs/github/code_intelligence.ts 16.45% <0%> (+0.17%) ⬆️
client/browser/src/libs/github/inject.tsx 17.46% <0%> (ø) ⬆️
...t/browser/src/libs/bitbucket/code_intelligence.tsx 17.54% <100%> (ø) ⬆️
.../browser/src/libs/phabricator/code_intelligence.ts 20.83% <100%> (ø) ⬆️
...lient/browser/src/libs/gitlab/code_intelligence.ts 33.33% <100%> (ø) ⬆️
client/browser/src/shared/util/dom.tsx 27.77% <44.44%> (+2.77%) ⬆️
...r/src/libs/code_intelligence/code_intelligence.tsx 58.62% <65%> (+0.99%) ⬆️
... and 92 more

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.