summaryrefslogtreecommitdiffstats
path: root/chromium/docs/website/site/developers/design-documents/instant/instant-support/index.md
blob: f339c9855f5a3814af2d603800d5b9b45abcb85c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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.