summaryrefslogtreecommitdiff
path: root/chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md')
-rw-r--r--chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md344
1 files changed, 344 insertions, 0 deletions
diff --git a/chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md b/chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md
new file mode 100644
index 00000000000..f339c9855f5
--- /dev/null
+++ b/chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md
@@ -0,0 +1,344 @@
+---
+breadcrumbs:
+- - /developers
+ - For Developers
+- - /developers/design-documents
+ - Design Documents
+- - /developers/design-documents/instant
+ - Instant
+page_name: instant-support
+title: Instant Support
+---
+
+**ALL CONTENT BELOW IS OUTDATED, INSTANT IS UNLAUNCHED**
+
+This document assumes some familiarity with the Chrome Instant feature,
+including the capabilities in Instant Extended. It's written primarily for
+developers working on the feature.
+
+## Instant URL
+
+An *Instant URL* is a URL that matches the instant_url template of the default
+search engine. So, given the default Chrome installation,
+"http://www.google.com/webhp" and "http://www.google.com/webhp?foo=bar#q=quux"
+are considered Instant URLs, whereas "http://www.google.com/accounts" is not.
+Instant Extended allows Instant URLs to match more template fields of the
+default search engine, with restrictions. So, in the extended mode,
+"https://www.google.com/?espv=1" and
+"https://www.google.com/search?espv=1&q=foo" are also Instant URLs, whereas the
+corresponding URLs without the "espv=1" parameter are not.
+
+**Why does this matter?**
+The Chrome Instant functionality only works with Instant URLs. So, in extended
+mode, we extract query terms from a URL into the omnibox only if the URL is an
+Instant URL. Similarly, we create an InstantTab (described below) only if the
+URL is an Instant URL.
+In addition, we try to bucket all Instant URLs (and only Instant URLs) into a
+dedicated renderer process (the *Instant renderer*). I say *try to* because we
+won't always succeed, as you'll see below. We also install the SearchBox
+extension only in the Instant renderer (and not other renderers). The SearchBox
+extension is responsible for implementing Chrome's side of the [SearchBox
+API](/searchbox), so this means that only webpages loaded in the Instant
+renderer can access the API.
+
+**Terminology note:** URLs can be classified into Instant URLs or non-Instant
+URLs. Webpages support Instant or they don't. In other words, to talk about
+Instant support, you need an actual page (WebContents). Throughout this
+document, we use the terms URL and page consistently in this manner. Of course
+pages have URLs, so we also say that "a page has an Instant URL" or "a
+non-Instant URL page".
+
+## Determining Instant Support
+
+**What is Instant support?**
+
+We want to know whether the page that we've loaded in the hidden Instant overlay
+actually supports Instant, i.e., the SearchBox API. We start by loading the
+default search engine's Instant URL into the overlay, but after it goes through
+redirects, it may end up in a page that may or may not support Instant.
+
+**Why do we want to determine Instant support?**
+Say the page isn't known to support Instant yet. When the user types in the
+omnibox, we should immediately fallback to the local omnibox popup. Doing
+otherwise is bad. If the user types, and we send `onchange()` to the page,
+hoping that it will eventually respond, we would be making the user wait.
+Conversely, if we know that the page supports Instant, we can and should send
+onchange() to it, instead of falling back to the local omnibox popup, since the
+local popup is an inferior experience.
+
+**How do we determine Instant support?**
+We start by assuming the page doesn't support Instant. At some point, the
+browser sends an IPC to the renderer. The SearchBox receives it, checks if
+`window.chrome.searchBox.onsubmit()` is a JS function defined in the page, and
+responds. When the response IPC is received by the browser, it thus determines
+Instant support.
+Also, if the browser receives an IPC at any point that's part of the SearchBox
+API (such as *SetSuggestions* or *ShowInstantPreview*), it considers the page to
+support Instant.
+Only pages that are Instant URLs can support Instant. A random non-Instant URL
+webpage can define the onsubmit() method, but there'll be no SearchBox extension
+available to receive or send the appropriate IPCs. In other words, a random
+webpage can't fool the browser into thinking it supports Instant (however, see
+the caveat in footnote \[1\]).
+
+**When do we determine Instant support?**
+The browser waits for the page to fully load and then sends the IPC mentioned
+above. This happens in the Instant implementation of
+`WebContentsObserver::DidFinishLoad()`.
+
+**Why don't we just check to see if the final page loaded in the Instant renderer?**
+First, even if the page is an Instant URL, it might not actually support
+Instant. For example, "http://www.google.com/webhp" might be a recognized
+Instant URL (so it gets assigned to the Instant renderer), but when it loads,
+the page may disable Instant due to server side experiments or other failures.
+Second, the page may go through one or more redirects that cause a
+*cross-process navigation* (see the OpenURLFromTab section below). If one of
+these redirects is renderer-initiated (e.g.: using `location.href = "..."` or a
+`<meta http-equiv=refresh>` tag), we'll still get a DidFinishLoad(). Say
+"http://www.google.com/webhp" (the Instant URL we initially load) uses a JS
+redirect to "http://www.google.com/accounts" (a non-Instant URL), which, after
+verifying your login cookies, redirects you back to the "/webhp" URL, again
+through a JS redirect. At some point, we'll get a DidFinishLoad() for the
+"/accounts" URL. If we check the renderer at that time, we will wrongly conclude
+that the page doesn't support Instant.
+In fact, we do actually send the request IPC after "/accounts" loads (because of
+DidFinishLoad(), as mentioned above). But by the time the response IPC comes
+back, the browser already knows about the JS redirect and is in the midst of
+handling it, so it knows that the response is for an older page, and ignores it.
+This is achieved by checking `WebContents::IsActiveEntry()` using the page_id
+contained in the response IPC.
+Of course, it's possible for the JS redirect to happen much later through a
+delayed timer, in which case, the "/accounts" URL is still the active entry when
+the browser receives the response IPC. In this case, we'll conclude that the
+page doesn't support Instant. This is okay since we can't possibly handle
+arbitrarily delayed redirects. Our method works for the common case of an
+immediate JS redirect.
+
+**What happens if the final page isn't an Instant URL? We will never get a response IPC, right?**
+True, since there's no SearchBox extension in that page's renderer. However, in
+the absence of a response IPC, the browser will continue to treat the page as
+not supporting Instant, so it all works out.
+
+## OpenURLFromTab
+
+`WebContentsDelegate::OpenURLFromTab()` is called whenever there's a
+cross-process navigation. This can happen in a few different ways:
+**Case 1:** The initial page load of the Instant URL in the hidden overlay may
+go through a series of HTTP redirects, any of which may trip the cross-process
+bit. Say we start by loading URL A. It redirects to URL B, which is "claimed" by
+an installed app (i.e., the app has listed B in its list of URL patterns in its
+manifest.json). Apps are loaded in separate renderer processes, so the redirect
+from A to B causes a cross-process navigation. HTTP redirects that are not
+claimed by any app do **not** trip the cross-process bit, even if they are not
+Instant URLs. I.e., if URL A is an Instant URL, but URL B is not, B will still
+be loaded in the same Instant renderer process that we started out with. \[1\]
+Alternatively, we'll hit the cross-process case if any of the redirects are
+renderer-initiated (as mentioned in the previous section, using location.href or
+a &lt;meta&gt; tag). Normally, renderer-initiated navigations (including
+redirects) are considered cross-process only if they actually cross an app or
+extension boundary (see `RenderViewImpl::decidePolicyForNavigation()`). For
+Instant however, we have added some code that makes **all** renderer-initiated
+navigations be initially treated as cross-process (see
+`ChromeContentRendererClient::ShouldFork()`).
+**Case 2:** Say we are showing the Instant overlay (URL A), and the user does
+something (such as click on a link) that causes the page to navigate to URL B,
+tripping a renderer-initiated cross-process navigation (as explained above).
+Assume that the click by itself doesn't cause the overlay to be committed
+(because the overlay is showing at less than full height).
+Let's look at what happens if B is not considered to be an Instant URL (in both
+cases above). In Case 2, we want to commit the overlay if possible, since it
+would be a mistake to try to continue using the overlay (B cannot support the
+Instant API, so the overlay would just stop working). However, in Case 1, we
+don't want to commit or discard the overlay just yet, since it's part of the
+initial series of redirects, and the final Instant page hasn't even been loaded
+yet.
+The way we distinguish these cases is by looking at whether the overlay is
+already known to support Instant. In Case 1, the Instant support determination
+hasn't yet been performed. In Case 2, it has, since we can't possibly be showing
+the overlay if it didn't support Instant. So, our algorithm for OpenURLFromTab()
+is this:
+
+```none
+OpenURLFromTab(url) {
+  if (!supports_instant_) {
+    // Case 1: Allow (perform) the navigation.
+    contents_->GetController().LoadURL(url);
+  } else {
+    // Case 2: Commit the overlay if possible. Allow or deny the navigation based on whether the commit succeeds or fails.
+    if (CommitIfPossible(...)) {
+      // The Browser is now the WebContentsDelegate, not us.
+      contents_->GetDelegate()->OpenURLFromTab(url);
+    } else {
+      // Deny (don't perform) the navigation.
+    }
+  }
+}
+```
+
+Note that when we get an OpenURLFromTab() call, it's incumbent upon us to
+actually perform the navigation. If we don't do anything, the navigation (or
+redirect) won't happen.
+
+**Wait, the above algorithm doesn't check whether `url` is a non-Instant URL, which is the reasoning given for distinguishing Case 2 above. Why not?**
+Actually, we want to commit the overlay on any user action, even if the
+navigation is to an Instant URL. This is because the user could've clicked on a
+link to say "http://www.google.com/webhp". The user expects the click to result
+in a committed tab, with the full webpage in it. It would be weird if it was
+still an overlay. The overlay isn't expected to randomly navigate on its own, so
+we'll assume that any call to OpenURLFromTab() is due to a user action (thus,
+committing the overlay is an appropriate action for us to take).
+
+**What happens if the overlay tries to update its hash state, i.e., the "#q=..."
+fragment of its URL, to keep state (i.e., without any user action)?**
+
+Thankfully, updating the fragment does not result in a call to OpenURLFromTab(),
+and thus we don't attempt to erroneously commit the overlay. TODO(sreeram): How
+about pushState?
+
+**If we can't commit, why do we not allow the navigation?**
+In the `else { // Deny }` part above, we could've chosen to still perform the
+navigation, and then do a new round of Instant support determination. Then, if
+the page ends up supporting Instant, we could keep using it as the overlay.
+Otherwise, we could discard it. This is needlessly complex and probably would
+introduce subtle bugs due to the second round of Instant support determination.
+Practically, this should never be needed. We *ought* to be able to commit all
+the time.
+The only time we wouldn't be able to commit the overlay is if the current
+omnibox text is a URL (and not a search). But in that case, the overlay
+shouldn't be showing any links other than suggestions (in particular, there
+should be no search results, search tools, Google+ widgets or such). If the user
+clicks on a suggestion, the page will request for it through the SearchBox API
+(*navigateContentWindow* for URLs and *show(reason=query)* for queries), so the
+page shouldn't cause an OpenURLFromTab() call. If something slips through the
+cracks (say because the overlay showed a "Learn more" link that the user
+clicked), we'll just disallow the navigation, which means the overlay remains
+showing the Instant page, and continues to work.
+
+## InstantTab
+
+In Instant Extended mode, an *InstantTab* represents a committed page (i.e., an
+actual tab on the tabstrip, and not an overlay) that's an Instant URL.
+Typically, this is either the server-provided NTP (New Tab Page) or a search
+results page. Such a page can also be used to show Instant suggestions and
+results, so it's appropriate to ask whether an InstantTab supports the Instant
+API.
+An InstantTab is a lightweight wrapper that's deleted and recreated as the user
+switches tabs (i.e., Instant only has a single InstantTab object, not one per
+tab). When the user switches to a tab with an Instant URL, we create an
+InstantTab wrapper around it, starting as usual by assuming that it doesn't
+support Instant. We then immediately send the request IPC. The rest of the
+Instant support determination works similar to the overlay. Since we (Instant)
+are not the WebContentsDelegate for a committed tab, none of the
+OpenURLFromTab() issues arise here. Note that we reset the InstantTab when the
+user switches tabs. We don't store the result of the Instant support
+determination anywhere permanently in the tab's WebContents.
+This generally works well, except for the following case: If the tab is a
+server-provided NTP ("http://www.google.com/webhp"), it's possible that we just
+created the WebContents and had to immediately commit it, so the page hasn't
+fully loaded yet. Since the common case is to open a browser with the NTP, we
+don't want to fall back to the local NTP just because we haven't finished
+loading the server-provided NTP. But, if the user starts typing into the omnibox
+before the NTP finishes loading, we don't want to wait for the server page
+(since that could take an arbitrary amount of time). In such a case (user typing
+before the NTP finishes loading), we'll fallback to the overlay (local omnibox
+popup).
+
+**Does that mean that we fallback to the overlay whenever the user types, but we haven't yet determined Instant support for an InstantTab?**
+No, that would mean practically every time the user switches to a tab and
+immediately starts typing, we'll end up with the overlay. This is obviously bad
+(because the user is bounced out of whatever search modes/tools they had in the
+tab).
+
+**Perhaps the solution is to store the Instant support bit with the WebContents? So that, when we switch to a tab, we won't have to perform the determination all over again?**
+This might help somewhat, but it doesn't solve all problems. For example, an
+Instant URL tab might have been created without Instant (i.e., through a link
+click or a bookmark navigation). When the user switches to it the first time, we
+still have to perform Instant support determination. Also, storing this bit with
+the WebContents means storing it somewhere within the SearchTabHelper, and this
+introduces unnecessary complexity (what should happen to that bit if the tab
+navigates in the background?) and leaks Instant concepts to an unrelated part of
+the codebase.
+
+**What's the solution then?**
+If we are on an InstantTab, and we haven't yet determined its Instant support,
+switch to the overlay only if the page hasn't finished loading. The reason this
+works is that, if the page has finished loading , we can go ahead and blindly
+send the onchange() to it as the user types. Note that we are not waiting for
+the page to load to determine Instant support. We send the IPC as soon as the
+user switches to the tab. Waiting for a page to load is the long pole that takes
+an indeterminate amount of time. Waiting for an IPC is a much smaller, mostly
+determinate amount of time (a few milliseconds, usually). So, if it turns out
+that the page doesn't support Instant, we'll quickly discover that and fallback
+to the overlay anyway.
+
+**What happens if the tab is a "sad tab", i.e., the page has crashed?**
+
+Right. We need to not wait for Instant support in that case.
+
+**What happens if the page isn't an Instant URL? Won't we end up waiting indefinitely for a response IPC that never arrives? So, won't we send onchange() into the void?**
+No. An InstantTab is only created for pages that are Instant URLs, so they
+should all have been bucketed into the Instant renderer process, and thus should
+have the SearchBox extension. So, a response IPC is guaranteed.
+
+Well, not really. Recall that a renderer-initiated navigation normally isn't
+cross-process unless it crosses an app boundary. So, it's possible that a tab
+started out with a non-Instant URL (thus, it was **not** assigned to the Instant
+renderer), then the user clicked on a link to an Instant URL. Now, this tab is
+eligible to be treated as an InstantTab. But the page is still in the
+non-Instant renderer, so we'll never get the response IPC back. This can only
+happen with InstantTab and not with the overlay, because the overlay always
+starts out with an Instant URL, and thus always starts out in the Instant
+renderer.
+
+So, putting all this together, here's our algorithm regarding InstantTab:
+
+```none
+ResetInstantTab() {
+  WebContents* contents = GetActiveWebContents();
+  if (contents->GetURL() is an Instant URL
+      AND contents hasn't crashed
+      AND contents->GetRenderViewHost()->GetProcess()->GetID() is an Instant renderer) {
+    overlay_.reset();  // Discard the overlay, if any.
+    instant_tab_.reset(contents);
+    instant_tab_->DetermineInstantSupport();  // Send the request IPC.
+  } else {
+    instant_tab_.reset();  // Don't use InstantTab.
+  }
+}
+Update() {
+  if (instant_tab_) {
+    if (overlay_) {
+      // We previously switched to the overlay because the InstantTab wasn't done loading.
+      // It probably has finished loading by now, but no matter. We'll continue using the overlay
+      // until it's discarded, to avoid a jarring switch as the user types.
+      overlay_->Update(...);
+    } else {
+      if (instant_tab_->contents()->IsLoading()) {
+        // The InstantTab hasn't finished loading. Use the overlay instead.
+        overlay_.reset(new InstantOverlay(...));
+        overlay_->Update(...);
+      } else {
+        // Use the InstantTab.
+        instant_tab_->Update(...);
+      }
+    }
+  } else {
+    // Use the overlay_, creating one if necessary.
+  }
+}
+ActiveTabChanged() {
+  overlay_.reset();  // Discard the overlay.
+  ResetInstantTab();
+}
+```
+
+Questions? Comments? Send them to sreeram@chromium.org.
+
+---
+
+\[1\] The astute reader would have observed that thus, it's technically possible
+for us to end up with a non-Instant URL page in the Instant renderer process,
+one which may even define the onsubmit() method and thus pass the Instant
+support determination test. This is fine. It can only happen if the Instant URL
+that we start with willfully redirects (using HTTP redirects) to such a
+non-Instant URL. \ No newline at end of file