Closed Bug 1344751 Opened 7 years ago Closed 7 years ago

optimize URL() read access in Worker threads for common URL schemes

Categories

(Core :: DOM: Workers, enhancement)

enhancement
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla56
Performance Impact high
Tracking Status
firefox56 --- fixed

People

(Reporter: bkelly, Assigned: catalinb)

References

(Blocks 1 open bug)

Details

Attachments

(3 files, 1 obsolete file)

In bug 1335900 comment 13 Catalin found URL() usage in a service worker was causing major perf problems for a real site.  This is because our Worker URL() class does a sync loop to the main thread for every operation because of bug 1228115.  Since these URLs are all going to be standard http/https we should be able to do better.

The ideas here are:

1) Make the constructor cache all read-only attribute values so they can be returned quickly.
2) Make the constructor try nsStdURLParser first
3) If nsStdURLParser fails, then fall back to the sync loop to main thread nsIURI
See Also: → 1228115
Assignee: nobody → amarchesini
(with permission)
Assignee: amarchesini → catalin.badea392
This is a huge footgun that we have that can hit sites that use the URL API in workers.
Whiteboard: [qf]
(In reply to Ben Kelly [reviewing, but slowly][:bkelly] from comment #0)
> 1) Make the constructor cache all read-only attribute values so they can be
> returned quickly.
> 2) Make the constructor try nsStdURLParser first
> 3) If nsStdURLParser fails, then fall back to the sync loop to main thread
> nsIURI

One thing to note about the fallback is that one example of such a case I think would be "about:blank" (I actually don't know what nsStdURLParser would do with that off the top of my head).  I don't think it's great to have such a huge performance cliff for parsing about:blank!  Given how sophisticated our simple URI parsing is <https://searchfox.org/mozilla-central/rev/596d188f6dbc8cb023e625f0a4310d184875f8fc/netwerk/base/nsSimpleURI.cpp#253>, we may as well roll our own code on the worker side to do that...

(It may be worthwhile to create a small utility that handles this and use it both for this bug and in <https://searchfox.org/mozilla-central/rev/596d188f6dbc8cb023e625f0a4310d184875f8fc/dom/cache/CacheStorage.cpp#101> and <https://searchfox.org/mozilla-central/rev/596d188f6dbc8cb023e625f0a4310d184875f8fc/dom/cache/TypeUtils.cpp#380> so that when bug 1332355 gets fixed we can port this single utility over to that API...)
In theory we can special case about:blank as well.
Also, I worry about creating a divergent URL implementation that only shows up on worker code.  The real fix is to deal with the main-thread-only nsIURI mess.
Whiteboard: [qf] → [qf:p1]
Using nsStdURLParser will result in writing or own implementation of nsStandardURL - the problem is that while we could just parse the string in the constructor and cache the results, using setters on the url object will either require more parsing (which leads us to implementing nsStandardURL all over again) or immediately falling back to using the main thread proxy.

If we could just keep an nsStandardURL object around, that will make things a lot easier. :baku told me that may not be possible at this time. Valentin, is nsStandardURL available on worker threads?
Flags: needinfo?(valentin.gosu)
(In reply to Cătălin Badea (:catalinb) from comment #6)
> Using nsStdURLParser will result in writing or own implementation of
> nsStandardURL - the problem is that while we could just parse the string in
> the constructor and cache the results, using setters on the url object will
> either require more parsing (which leads us to implementing nsStandardURL
> all over again) or immediately falling back to using the main thread proxy.

Well, avoiding the main thread round-trip for read-only access would be a nice first step.  This would avoid the perf penalty for people using URL() in their fetch event routing code.
I agree that this is less than optimal.
This problem is that JS addons can implement nsIURI forcing it to be main-thread only. We will get rid of this restriction after Fx57, which will allow us some flexibility in trying to fix the issue.

Now, nsStandardURL isn't thread safe - but there are a few workarounds for the issue.
1. If we create and destroy the object on the main thread, and hold it in a nsMainThreadPtrHandle, then we can technically use it on other threads (if we make sure there are no data races).
2. Another option would be to implement a threadsafe URL parser, to which nsStandardURL would forward calls. The worker would of course need to use the threadsafe implementation.
Flags: needinfo?(valentin.gosu)
(In reply to Valentin Gosu [:valentin] from comment #8)
> I agree that this is less than optimal.
> This problem is that JS addons can implement nsIURI forcing it to be
> main-thread only. We will get rid of this restriction after Fx57, which will
> allow us some flexibility in trying to fix the issue.
> 
> Now, nsStandardURL isn't thread safe - but there are a few workarounds for
> the issue.
> 1. If we create and destroy the object on the main thread, and hold it in a
> nsMainThreadPtrHandle, then we can technically use it on other threads (if
> we make sure there are no data races).
> 2. Another option would be to implement a threadsafe URL parser, to which
> nsStandardURL would forward calls. The worker would of course need to use
> the threadsafe implementation.

What about nsStandardURL with nsStdURLParser?
(In reply to Cătălin Badea (:catalinb) from comment #9)
> What about nsStandardURL with nsStdURLParser?

This is sort of OK to do, but as you said, we are duplicating functionality. Also, apart from calling to nsStdURLParser, nsStandardURL also handles percent encoding, IDNA, etc. Not just parsing a URL.
Note that nsStandardURL itself is also not thread-safe, for example it accesses a global gIDN, I didn't look for more.  I suggest for now improving the performance of the constructors and the getters, and maybe punt on the rest for a different bug?
since trying nsStandardURL first would always succeed. Which brings us to the
sketchy part. I don't see why nsStandardURL is restricted to the main thread.
My understanding is that nsStandardURL defaults to either nsStdURLParser or
RustURL and there are no assertions enforcing main thread instantiation.

Valentin could you please have a look at this patch and if I'm breaking thread
safety with nsStandardURL explain why/how?
Attachment #8876129 - Flags: feedback?(valentin.gosu)
Attachment #8876129 - Attachment description: I had to rely on the scheme to choose which path to take when parsing a new url, → Bug 1344751 - use nsStandardURL for http and https in workers. r=baku
ugh, made a mess of the attachment's description. Sorry.

The patch uses nsStandardURL for http/https and bounces to the main thread for anything else.
I had to check for the scheme because just trying out nsStandardURL first would always succeed.
(In reply to :Ehsan Akhgari (needinfo please, extremely long backlog) from comment #11)
> Note that nsStandardURL itself is also not thread-safe, for example it
> accesses a global gIDN, I didn't look for more.  I suggest for now improving
> the performance of the constructors and the getters, and maybe punt on the
> rest for a different bug?

The only implementation of nsIIDNService has thread safe ref counting. I don't need nsStandardURL to be thread safe, just
usable on worker threads.
> The only implementation of nsIIDNService has thread safe ref counting

That doesn't matter.  Threadsafe refcounting just means that you can refcount it on multiple threads, not _use_ it.  Whether you can _use_ it depends on what it actually does.

Looking as nsIDNService, its Init() method is certainly not safe to call from off main thread, for example.

Furthermore, it has a pref observer, which presumably runs on the main thread.  This changes a bunch of members, without locking.  Any method that uses any of those members is presumably not OK to use from off the main thread either.

OK, so now nsStandardURL.  As ehsan said, it has a global gIDN.  This is lazily allocated in nsStandardURL::NormalizeIDN.  At the very least you'd have to ensure that this is always called (on main thread!) before any nsStandardURL creation happens on workers.  It's possible that this is already the case, since it looks like nsStandardURL::BuildNormalizedSpec always calls it.

But NormalizeIDN also calls ConvertToDisplayIDN, which touches (directly, and via other methods it calls) some of those members that are poked unlocked on the main thread on pref changes.

So here's an example of how you're breaking threadsafety.

Generally, though, the burden of proof is to show how we are NOT breaking threadsafety when we have a situation like this rather than expecting reviewers to prove that we are breaking it.
Comment on attachment 8876129 [details] [diff] [review]
Bug 1344751 - use nsStandardURL for http and https in workers. r=baku

IRC talks led to the conclusion that it's best to change nsStandardURL to be able handle being used on a different thread.
Attachment #8876129 - Flags: feedback?(valentin.gosu)
Fixed a bunch of failures on tryserver.
Attachment #8876129 - Attachment is obsolete: true
nsStandardURL::InitGlobals should be called always be called on main thread.
Constructors called on other threads will dispatch a sync runnable to MT to init
the globals. In practice, I think this should never happen. Not sure how to test
this code, though.

gRustEnabled is still read without a lock on worker threads, but I think that's
acceptable.

nsIDNService can only be created on the main thread and all the observer bits
are main thread only. Members that are dependent on pref changes are updated
behind a lock on the main thread. Worker threads need to acquire this lock before
reading these values.
Attachment #8878060 - Flags: review?(valentin.gosu)
Attachment #8878061 - Flags: review?(valentin.gosu) → review+
Comment on attachment 8878060 [details] [diff] [review]
Make nsStandardURL and nsIDNService available on worker threads.

Review of attachment 8878060 [details] [diff] [review]:
-----------------------------------------------------------------

::: netwerk/base/nsStandardURL.cpp
@@ +329,5 @@
>      , mSupportsFileURL(aSupportsFileURL)
>  {
>      LOG(("Creating nsStandardURL @%p\n", this));
>  
> +    InitGlobalObjectsIfNeeded();

Is this better? If this method is not inlined we'd still be making an extra function call in every constructor.

@@ +408,5 @@
> +
> +    nsCOMPtr<nsIIDNService> serv(do_GetService(NS_IDNSERVICE_CONTRACTID));
> +    if (serv) {
> +        NS_ADDREF(gIDN = serv.get());
> +    }

Add a MOZ_ASSERT(gIDN) - I don't expect this to fail, but it's best to make sure.
Attachment #8878060 - Flags: review?(valentin.gosu) → review+
Attachment #8878047 - Flags: review?(amarchesini)
Comment on attachment 8878047 [details] [diff] [review]
use nsStandardURL for http and https in workers.

Review of attachment 8878047 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/fetch/Request.cpp
@@ +316,5 @@
>      }
>      if (NS_WARN_IF(aRv.Failed())) {
>        return nullptr;
>      }
> +    fprintf(stderr, "CATALINB: %s\n", NS_ConvertUTF16toUTF8(requestURL).get());

Remove this.

::: dom/url/URLWorker.cpp
@@ +621,5 @@
> +      aRv.ThrowTypeError<MSG_INVALID_URL>(aURL);
> +      return;
> +    }
> +    rv = net_ExtractURLScheme(NS_ConvertUTF16toUTF8(aBase.Value()), scheme);
> +    if (NS_FAILED(rv)) {

NS_WARN_IF

@@ +633,5 @@
> +    RefPtr<nsStandardURL> baseURL;
> +    if (aBase.WasPassed()) {
> +      baseURL = new nsStandardURL();
> +      rv = baseURL->SetSpec(NS_ConvertUTF16toUTF8(aBase.Value()));
> +      if (NS_FAILED(rv)) {

NS_WARN_IF

@@ +692,5 @@
>  
>  void
>  URLWorker::SetHref(const nsAString& aHref, ErrorResult& aRv)
>  {
> +  nsCString scheme;

nsAutoCString ?

@@ +710,5 @@
> +      RefPtr<TeardownURLRunnable> runnable =
> +        new TeardownURLRunnable(mURLProxy);
> +      mURLProxy = nullptr;
> +
> +      if (NS_FAILED(NS_DispatchToMainThread(runnable))) {

just do a simple:

if (NS_WARN_IF(NS_FAILED(...))) {
  return;
}

@@ +734,5 @@
> +
> +    if (runnable->Failed()) {
> +      aRv.ThrowTypeError<MSG_INVALID_URL>(aHref);
> +      return;
> +    }

return;

@@ +735,5 @@
> +    if (runnable->Failed()) {
> +      aRv.ThrowTypeError<MSG_INVALID_URL>(aHref);
> +      return;
> +    }
> +  } else {

remove this "} else {"

@@ +823,5 @@
>  
>    MOZ_ASSERT(!runnable->Failed());
>  }
>  
> +#define STDURL_GETTER(value, func )   \

1. extra space after func
2. rename 'func' to 'method'

@@ +851,5 @@
>  void
>  URLWorker::SetUsername(const nsAString& aUsername, ErrorResult& aRv)
>  {
> +  if (mStdURL) {
> +    aRv = mStdURL->SetUsername(NS_ConvertUTF16toUTF8(aUsername));

what about having a macro also for setters? They are all equal.
Attachment #8878047 - Flags: review?(amarchesini) → review+
Pushed by catalin.badea392@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/2a386ea17990
use nsStandardURL for http and https in workers. r=baku
https://hg.mozilla.org/integration/mozilla-inbound/rev/976a4789deea
Make nsStandardURL and nsIDNService available on worker threads. r=valentin
https://hg.mozilla.org/integration/mozilla-inbound/rev/040e1c1c53dc
nsStandardURL::SetSpec should return error on empty scheme. r=valentin
Pushed by catalin.badea392@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/359dbef1ae54
use nsStandardURL for http and https in workers. r=baku
https://hg.mozilla.org/integration/mozilla-inbound/rev/08fdd3066fde
Make nsStandardURL and nsIDNService available on worker threads. r=valentin
https://hg.mozilla.org/integration/mozilla-inbound/rev/dee772828d50
nsStandardURL::SetSpec should return error on empty scheme. r=valentin
Backed out for failing xpcshell's chrome/test/unit/test_no_remote_registration.js:

https://hg.mozilla.org/integration/mozilla-inbound/rev/0908f7d7e70140d0a7d4ed479cb3e73779193b18
https://hg.mozilla.org/integration/mozilla-inbound/rev/a2ed9b85083f108f201b8129ea01e215bc2df369
https://hg.mozilla.org/integration/mozilla-inbound/rev/c107e314c7397c30ae4417c3ba94f8c1a45d5d53

Push with failures: https://treeherder.mozilla.org/#/jobs?repo=mozilla-inbound&revision=dee772828d502344d59deff11c41f16c7ea934a7&filter-resultStatus=testfailed&filter-resultStatus=busted&filter-resultStatus=exception&filter-resultStatus=retry&filter-resultStatus=usercancel&filter-resultStatus=runnable
Failure log: https://treeherder.mozilla.org/logviewer.html#?job_id=111941632&repo=mozilla-inbound

[task 2017-07-05T13:24:56.956174Z] 13:24:56     INFO -  TEST-START | chrome/test/unit/test_no_remote_registration.js
[task 2017-07-05T13:24:57.688792Z] 13:24:57  WARNING -  TEST-UNEXPECTED-FAIL | chrome/test/unit/test_no_remote_registration.js | xpcshell return code: 0
[task 2017-07-05T13:24:57.690578Z] 13:24:57     INFO -  TEST-INFO took 734ms
[task 2017-07-05T13:24:57.693558Z] 13:24:57     INFO -  >>>>>>>
[task 2017-07-05T13:24:57.695535Z] 13:24:57     INFO -  PID 7214 | [7214] WARNING: Couldn't get the user appdata directory. Crash events may not be produced.: file /home/worker/workspace/build/src/toolkit/crashreporter/nsExceptionHandler.cpp, line 2864
[task 2017-07-05T13:24:57.697320Z] 13:24:57     INFO -  (xpcshell/head.js) | test MAIN run_test pending (1)
[task 2017-07-05T13:24:57.699731Z] 13:24:57     INFO -  PID 7214 | XULAppInfo is already registered. Storing currently registered object for restoration later.Testing protocol 'moz-protocol-ui-resource' with type 'content'
[task 2017-07-05T13:24:57.701845Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-ui-resource' with type 'locale'
[task 2017-07-05T13:24:57.704331Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-ui-resource' with type 'skin'
[task 2017-07-05T13:24:57.706172Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-ui-resource' with type 'override'
[task 2017-07-05T13:24:57.709221Z] 13:24:57     INFO -  TEST-PASS | chrome/test/unit/test_no_remote_registration.js | run_test - [run_test : 208] "moz-protocol-ui-resource://foo/" != "file:///home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/data/bar/override-moz-protocol-ui-resource.xul"
[task 2017-07-05T13:24:57.711332Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-ui-resource' with type 'resource'
[task 2017-07-05T13:24:57.713415Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-file' with type 'content'
[task 2017-07-05T13:24:57.715882Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-file' with type 'locale'
[task 2017-07-05T13:24:57.718305Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-file' with type 'skin'
[task 2017-07-05T13:24:57.720351Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-file' with type 'override'
[task 2017-07-05T13:24:57.723140Z] 13:24:57     INFO -  TEST-PASS | chrome/test/unit/test_no_remote_registration.js | run_test - [run_test : 208] "moz-protocol-local-file://foo/" != "file:///home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/data/bar/override-moz-protocol-local-file.xul"
[task 2017-07-05T13:24:57.725238Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-file' with type 'resource'
[task 2017-07-05T13:24:57.727364Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-loadable-by-anyone' with type 'content'
[task 2017-07-05T13:24:57.729796Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-loadable-by-anyone' with type 'locale'
[task 2017-07-05T13:24:57.732313Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-loadable-by-anyone' with type 'skin'
[task 2017-07-05T13:24:57.734237Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-loadable-by-anyone' with type 'override'
[task 2017-07-05T13:24:57.737839Z] 13:24:57     INFO -  TEST-PASS | chrome/test/unit/test_no_remote_registration.js | run_test - [run_test : 208] "moz-protocol-loadable-by-anyone://foo/" != "file:///home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/data/bar/override-moz-protocol-loadable-by-anyone.xul
[task 2017-07-05T13:24:57.740512Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-loadable-by-anyone' with type 'resource'
[task 2017-07-05T13:24:57.744648Z] 13:24:57     INFO -  PID 7214 | Testing protocol 'moz-protocol-local-resource' with type 'content'
[task 2017-07-05T13:24:57.747408Z] 13:24:57     INFO -  PID 7214 | spec=moz-protocol-local-resource.xul
[task 2017-07-05T13:24:57.749527Z] 13:24:57     INFO -  PID 7214 | JavaScript error: /home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/test_no_remote_registration.js, line 29: NS_ERROR_MALFORMED_URI: Component returned failure code: 0x804b000a (NS_ERROR_MALFORMED_URI) [nsIURI.spec]
[task 2017-07-05T13:24:57.752765Z] 13:24:57     INFO -  PID 7214 | [Exception... "Component returned failure code: 0x804b000a (NS_ERROR_MALFORMED_URI) [nsIURI.spec]"  nsresult: "0x804b000a (NS_ERROR_MALFORMED_URI)"  location: "JS frame :: /home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/test_no_remote_registration.js :: newURI :: line 29"  data: no]
[task 2017-07-05T13:24:57.755880Z] 13:24:57     INFO -  Should have registered our URI for protocol moz-protocol-local-resource
[task 2017-07-05T13:24:57.758414Z] 13:24:57     INFO -  /home/worker/workspace/build/tests/xpcshell/tests/chrome/test/unit/test_no_remote_registration.js:run_test:214
[task 2017-07-05T13:24:57.760713Z] 13:24:57     INFO -  /home/worker/workspace/build/tests/xpcshell/head.js:_execute_test:543
[task 2017-07-05T13:24:57.763546Z] 13:24:57     INFO -  -e:null:1
[task 2017-07-05T13:24:57.765302Z] 13:24:57     INFO -  exiting test
This also causes devtools failures: https://treeherder.mozilla.org/logviewer.html#?job_id=111943075&repo=mozilla-inbound
> TEST-UNEXPECTED-FAIL | devtools/client/jsonview/test/browser_jsonview_save_json.js | File picker should provide the correct default file extension. - Got null, expected json
Pushed by catalin.badea392@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/10c975d4a8f9
use nsStandardURL for http and https in workers. r=baku
https://hg.mozilla.org/integration/mozilla-inbound/rev/ec02a5ecb2b4
Make nsStandardURL and nsIDNService available on worker threads. r=valentin
Had to drop the changes I made to nsStandardURL::SetSpec since the previous behaviour was expected in a bunch of different places and fixing it wasn't straightforward.
Flags: needinfo?(catalin.badea392)
(In reply to Wes Kocher (:KWierso) from comment #29)
> Backed this out for frequent android crashes like
> https://treeherder.mozilla.org/logviewer.html#?job_id=113116393&repo=mozilla-
> inbound
> 
> https://hg.mozilla.org/integration/mozilla-inbound/rev/
> 62e0fae2a0efc8a4ea4df9b411e11099683c73a1

Isn't this the crash from bug 1375659?
Flags: needinfo?(catalin.badea392) → needinfo?(wkocher)
Pushed by catalin.badea392@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/f87c65cf26de
use nsStandardURL for http and https in workers. r=baku
https://hg.mozilla.org/integration/mozilla-inbound/rev/9cb4e53f36aa
Make nsStandardURL and nsIDNService available on worker threads. r=valentin
https://hg.mozilla.org/mozilla-central/rev/f87c65cf26de
https://hg.mozilla.org/mozilla-central/rev/9cb4e53f36aa
Status: NEW → RESOLVED
Closed: 7 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla56
FYI, this shows a nice improvement in time to firing fetch event in telemetry:

https://mzl.la/2uyua2k

Our total time for interception did not improve, though, because ultimately we do end up waiting on main thread again later in the process.  This should still help avoid head-of-line blocking and allow more parallelism in total.  Also it clears the way to remove more main thread blocking.
Performance Impact: --- → P1
Whiteboard: [qf:p1]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: